From bae9228110e1d816a7c4f971891422420611fb55 Mon Sep 17 00:00:00 2001 From: Viral Vala Date: Mon, 26 Jul 2021 19:31:02 +0530 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 4be0c87c7498228e5d773129dd1686c4119a1580 Author: Viral Vala Date: Mon Jul 26 18:38:47 2021 +0530 Adding test cases for domain filtering and commenting filter vast tags for adpod commit 07d1b4c6d5a6ea45d486d798ee0e984748567e14 Author: Viral Vala Date: Thu Jul 22 15:46:44 2021 +0530 OTT-241 URL Encoding All Values of Macro commit 5911dd04c72dff7cb700f3c50639e6b44bbfe7a2 Author: Viral Vala Date: Tue Jul 20 14:21:52 2021 +0530 merged master branch into OTT-48 commit e6fb5fb0115221f0ebf95437bfb73cbd847c3957 Author: PubMatic-OpenWrap Date: Fri Jul 16 00:34:00 2021 +0530 OTT-233 : Fix error code for VAST bidders when bid price is zero commit 6160f6424c63c8c19da9429dd2650a9d99714c21 Author: mohammad-murad Date: Thu Jul 15 11:10:50 2021 +0530 OTT-232 : Fix incorrect tagID in final VastTag URLs Signed-off-by: mohammad-murad commit 708f2e65923a883e090116c871c720ad2cdcde12 Author: PubMatic-OpenWrap Date: Mon Jul 5 15:26:27 2021 +0530 OTT-217 - Remove logger for excluded VASTTags commit d755118080e4e3d835bc54253a597df4da2ee45d Author: Viral Vala Date: Wed Jun 30 14:27:09 2021 +0530 OTT-48: Fixing Panic Issue commit fa61146b61752587ddf7b600d16a186752dc6ef7 Author: Viral Vala Date: Tue Jun 29 15:21:25 2021 +0530 Resolving Test Cases commit db2b45ab35f032e84a606dd9fb474a26e6f517bf Author: Viral Vala Date: Mon Jun 28 22:23:25 2021 +0530 OTT-162 Adding BlockedVASTTags in Bid Response commit f1f8712d6c990bc02bf476afcca5092edf5595cb Author: mohammad-murad Date: Mon Jun 28 17:43:49 2021 +0530 OTT-214 : Fix panic while getting debug info http calls Signed-off-by: mohammad-murad commit ff2b05c0234bdb594e41f0d1c90cc9fca17f63f7 Author: mohammad-murad Date: Wed Jun 23 14:23:40 2021 +0530 OTT:161 - Filter VAST tags based on duration Signed-off-by: mohammad-murad commit 58d0a4ee5194c3b21ac95d8b973f2dcbd8e886a8 Merge: fb6d3c22 ca9b117b Author: Viral Vala Date: Fri Jun 18 20:03:35 2021 +0530 Merge branch 'master' into OTT-48 commit fb6d3c22f1adcaee2560a714e5502146ce05d109 Author: ShriprasadM Date: Mon Jun 7 17:07:47 2021 +0530 OTT-205: Added temporary change for enable QA to debug and verify the changes for OTT-159 (#166) Co-authored-by: Shriprasad commit 1def51cb6b4b9508b0bf1f870273350383341dd4 Author: Viral Vala Date: Thu May 20 10:14:22 2021 +0530 OTT-48 Fixing formatting of OTT-48 based on master branch commit cafa88fd85b290922c02efee3bc3ab15fe47f1aa Author: Viral Vala Date: Tue May 18 22:47:03 2021 +0530 OTT-48 Using PubMatic-OpenWrap etree library for vast xml processing commit 101f1316f0ee321b045f75f57e3603fe12f2a523 Author: Viral Vala Date: Tue May 18 20:52:05 2021 +0530 merged master branch into OTT-48 commit 6789fa2da4f9887382cfa9151c3e7e2034dd0f1a Author: Viral Vala Date: Mon May 17 19:05:38 2021 +0530 OTT-159,OTT-160 Adding Duration and VASTTag Details in ExtBidPrebidVideo Object commit bd352c2288e679094399590f30763e95175f512c Author: Viral Vala <63396712+pm-viral-vala@users.noreply.github.com> Date: Mon May 3 14:15:55 2021 +0530 OTT-57,OTT-153 Mocking VAST Tag Details and Update Request Handler fo… (#147) * OTT-57,OTT-153 Mocking VAST Tag Details and Update Request Handler for VAST Tag * OTT-57 Incorporating Review Comments commit 8388840a1a0731e0417e674db5eb8f8f93106635 Author: Viral Vala Date: Fri Apr 16 10:01:52 2021 +0530 resolving master merge issue commit 4a88c613008029c119825f7db7a7d89f0783382d Author: Viral Vala Date: Tue Apr 13 12:19:28 2021 +0530 Resolving Merge Issue commit c01ab29343a57479d8af66f37bead848ce44a6b8 Author: Viral Vala Date: Mon Apr 12 14:29:22 2021 +0530 merging master to OTT-48 branch commit d61bacbae3278faa4a786f6de5f58724b12b805e Author: mohammad-murad Date: Tue Feb 23 16:54:33 2021 +0530 OTT-100 : Get advertiser list from VAST Signed-off-by: mohammad-murad commit 3db84428d1d7d0e8a788b254a48cd72c3a244b82 Author: Shriprasad Date: Tue Feb 16 16:18:37 2021 +0530 OTT-48: Enabled running all tests for TestApplyAdvertiserBlocking and corrected syntax error commit 54b073ec39ac316bb2e176bcd634d832f85ad8d4 Merge: b76d1a45 6fcee647 Author: Shriprasad Date: Tue Feb 16 16:02:49 2021 +0530 Merge branch 'master' into OTT-48 commit b76d1a45ab1210eae4466ae2ebff2aa6578243bc Author: ShriprasadM Date: Wed Feb 10 16:40:09 2021 +0530 OTT-101: Added exchange.applyAdvertiserBlocking and unit tests around it (#108) * OTT-101: Added exchange.applyAdvertiserBlocking and unit tests around it * OTT-101: Unit tests added with tested workflow. Integrated the function * OTT-101: Removed check * OTT-101: Corrected test * OTT-101: Removed publicsuffix.tld+1. Instead we will return domain irrepstive of level. Renamed adjustDomain to normalizeDomain. Added and modified few test cases * OTT-101: Temporary method vastbidder.getAdvertisers and its integration added for making this drop testable. OTT-100 must replace this function and its integration with valid implementation Also change the way of checking if bidder is instance of vasttag bidder. There was issue in determining it while doing the integration testing * OTT-101: Added new bucket in adaptermap - tagbidders. This map will hold the information around tagbidders. This bucket would be used by ApplyAdvertiserBlocking function for checking if bid belongs to ApplyAdvertiserBlocking. Also Replaced Index method with HasPrefix and also added case-insenstive comparison * OTT-101: Change the way of detecting the bidder instance. Now bidder will be resolved using resolveBidder method based on alias * OTT-101: Added publicsuffix check * OTT-101:Refectored the logic. Added more unit tests * OTT-101: Corrected the BidID and added one more bid request for testing Co-authored-by: Shriprasad commit 51dabf46dd505d1eab61820f7806e032c60d4088 Author: Viral Vala Date: Wed Feb 3 15:44:02 2021 +0530 Merged 'master' branch into OTT-48 commit ea145cf3a6b709ca15f3961f44ba889adc4e9de3 Author: Viral Vala Date: Wed Jan 27 17:22:47 2021 +0530 OTT-54 Updating longitude macro name from long to lon commit 154a368b971f300bfa6a9d3d1dc0b8bcec9b2ac2 Author: ShriprasadM Date: Wed Jan 27 15:21:36 2021 +0530 OTT-88: Supporting Request Headers for Each VAST Bidder (#106) * OTT-88: Header changes * OTT-88: Added unit tests * OTT-88: Added support for VAST protocol based header set * OTT-88: Added unit test for checking if default + custom headers are set in requestData while making tag bidder request * OTT-88: Assigned value to IBidderMacro * OTT-88 fixing gofmt for vast bidder files * OTT-88: Added support for logging http request headers inside ext.debug.httpcalls..requestheaders * Merge branch 'OTT-48' into OTT-88 * OTT-88: Fixed test case issue * OTT-88: Reverted the comment * OTT-88: Code review changes are addressed * OTT-88: Refactored. Replace switch with if-else for better readbility. Added unit test when protocols contains vast 2 and vast 4 both Co-authored-by: Shriprasad Co-authored-by: Viral Vala commit a8cb5f9857bdbf9dd535ffedee84f19eaae27f5b Author: Viral Vala Date: Tue Jan 19 23:26:11 2021 +0530 OTT-54 Updating scripts/coverage.sh to Skip prebid-server for coverage check commit b390e5ae06b6fbb445e800dea30ba84b3ec85e6a Author: Viral Vala Date: Tue Jan 19 23:12:33 2021 +0530 OTT-54 Fixing gofmt Formatting commit 531671c6d3bfd6ddf40f135d1f812f06d2b4ea46 Author: Viral Vala Date: Tue Jan 19 22:59:54 2021 +0530 OTT-54: Adding Missing TestCase Files commit 1fea95e3550757e502b5d79faa33e5ca14ac5d18 Author: Viral Vala Date: Tue Jan 19 22:46:28 2021 +0530 OTT-54 Adding TestCases commit 74758d7fbd2efa481e9fc833397b9fbc5c1ba3fe Author: Viral Vala Date: Tue Jan 19 13:03:36 2021 +0530 OTT-54 Fixing Bugs of Macro commit c5e9530b3ad547ef82f2d46e1b5996c0b1d5e93f Author: Viral Vala Date: Mon Jan 18 13:33:08 2021 +0530 Merge branch 'master' into OTT-48 commit a17348d11db6bf0a10336d32f45d874b0a8324ec Author: Viral Vala Date: Tue Jan 12 12:17:54 2021 +0530 Merging OTT-55 to OTT-48 commit 172b374019f4fd1692ede52230b01bca35d97478 Merge: 776a1f3b e012115a Author: Viral Vala Date: Tue Jan 12 12:12:57 2021 +0530 Merge branch 'OTT-55' into OTT-48 commit e012115a547061d3b1256bfe96240a83a4cadfc8 Merge: 661db68c fde6dc47 Author: Viral Vala Date: Tue Jan 12 12:10:42 2021 +0530 Merge branch 'OTT-54' into OTT-55 commit fde6dc47a553f36aaa62a724f8bf554e566f7565 Author: Viral Vala Date: Tue Jan 12 12:00:04 2021 +0530 OTT-54 Adding Default VAST Bidder Implementation commit 776a1f3ba8e3c9e2175d08efbb82f1a39879466d Author: Viral Vala Date: Fri Jan 8 15:47:02 2021 +0530 Merging OTT-54, OTT-55, OTT-58 OTT-58: Extract Duration and Creative Id and update Prebid Server objects (#95) commit 661db68c86dc47ac4d0a59468bea6273d5824f37 Author: Viral Vala Date: Fri Jan 8 10:30:14 2021 +0530 OTT-54: Removing Bidder Config commit c9e9c82f1c72dc35b68a839f8cdf19631c97165d Author: Viral Vala Date: Fri Jan 8 10:27:22 2021 +0530 removing bidder config commit 12a87111be89970e5ee1b742bdcc0cecf15b355c Author: Viral Vala Date: Mon Jan 4 16:41:24 2021 +0530 OTT-55 Reverting Merge Conflicts commit 90fe4c5a380e6a8f74be5bce0ed1feac5cb40c46 Merge: b17213cc 9f692028 Author: Viral Vala Date: Mon Jan 4 14:28:40 2021 +0530 Merge branch 'OTT-54' into OTT-55 commit 9f69202886ee656cc101603dc26748e184d9629e Author: Viral Vala Date: Tue Dec 29 12:27:41 2020 +0530 OTT-54 Adding Macro Support for Tag Bidder commit b17213cc6257ba5b477462a122f0e4d0ad92adf1 Author: ShriprasadM Date: Mon Jan 4 12:39:46 2021 +0530 OTT-58: Extract Duration and Creative Id and update Prebid Server objects (#95) * OTT-58: Added empty function getBidDuration. Added unit tests around it * OTT-58: Few trials for converting VAST bid duration into seconds when input is HH:MM:SS.mmm * OTT-58: Added unit tests and functionality for determining the video ad duration * OTT-58: Refactored and added some more unit tests * OTT-58: Added Benchmark testcase * OTT-58: Removed version argument from tests. Its not required for now. Fixed test issue * OTT-53: typo fix * OTT-58: Modifed regexp for millis to accept value max upto 999. Added unit tests around it * OTT-58: Added handling for detecting creative.id and updating it in typedBid.Bid.CrID * OTT-58: Addressed code review comments * OTT-58: Reverted changes for getCreativeID. It will now return empty string to creative id is not present. Instead caller will generate the random creative id with cr as prefix Co-authored-by: Shriprasad commit cfa29dce12a79ebb8b623382a073be2c8e30f4cb Author: Viral Vala Date: Sat Dec 19 09:36:38 2020 +0530 added creative id parsing commit bc87d853ede3774d24292387ff81bc7d0fc62014 Author: Viral Vala Date: Sat Dec 19 09:15:39 2020 +0530 updating macro processor commit 40ebadaf47efb7e7cb71d58657ac27204f67d901 Author: Shriprasad Date: Fri Dec 18 17:25:30 2020 +0530 Revert "OTT-55: Added empty function getBidDuration. Added unit tests with expectations around it" This reverts commit 417f6fdb9247b566b2c1ef8c2bbd93b742e88507. commit 417f6fdb9247b566b2c1ef8c2bbd93b742e88507 Author: Shriprasad Date: Fri Dec 18 17:21:43 2020 +0530 OTT-55: Added empty function getBidDuration. Added unit tests with expectations around it commit ebcb2216cd970f891ec0317327f70a22365df0e5 Author: Viral Vala Date: Fri Dec 18 13:59:13 2020 +0530 fixing bugs commit 808264a125348974ccf50f742c91bae41c60d394 Author: Viral Vala Date: Fri Dec 18 11:33:09 2020 +0530 updated macro format commit dda1e87c215433e503e86475811521e0ab6eeb11 Author: Viral Vala Date: Fri Dec 18 11:11:45 2020 +0530 testing phase commit a439fd4d09132f95d73fdd495fd5026ef94efc9f Author: Viral Vala Date: Wed Dec 16 09:45:27 2020 +0530 refactoring code commit 5cbf91e9124435083f8f6627ba49844f4273ca2b Author: Viral Vala Date: Mon Dec 14 19:30:40 2020 +0530 refactored code commit 26c06194f0919a6978e751a8c98d8e28cf5f4507 Author: Viral Vala Date: Sun Dec 13 00:22:19 2020 +0530 refactoring code commit 3dade2ea283b37f4999ae059aadf19c6a71cc4fc Author: Viral Vala Date: Wed Dec 2 17:35:27 2020 +0530 OTT-55 Adding Macro Definitions commit 81841e5dd2a651cc2fa9d9a5dc0cc2167a7574f8 Author: Viral Vala Date: Tue Dec 1 17:45:31 2020 +0530 OTT-55 Second Commit commit 911db3e56980d2cd322fd88d3fd54e1808aaacd3 Author: Viral Vala Date: Wed Nov 25 12:28:34 2020 +0530 OTT-55 First Commit --- adapters/bidder.go | 6 + adapters/vastbidder/bidder_macro.go | 1233 ++++++++++++++++ adapters/vastbidder/bidder_macro_test.go | 1258 +++++++++++++++++ adapters/vastbidder/constant.go | 166 +++ adapters/vastbidder/ibidder_macro.go | 199 +++ adapters/vastbidder/itag_response_handler.go | 43 + adapters/vastbidder/macro_processor.go | 216 +++ adapters/vastbidder/macro_processor_test.go | 587 ++++++++ adapters/vastbidder/mapper.go | 180 +++ adapters/vastbidder/sample_spotx_macro.go.bak | 28 + adapters/vastbidder/tagbidder.go | 87 ++ adapters/vastbidder/tagbidder_test.go | 149 ++ adapters/vastbidder/util.go | 70 + .../vastbidder/vast_tag_response_handler.go | 334 +++++ .../vast_tag_response_handler_test.go | 385 +++++ config/config.go | 2 + endpoints/events/vtrack.go | 3 +- endpoints/events/vtrack_test.go | 3 +- endpoints/openrtb2/ctv/types/adpod_types.go | 10 +- endpoints/openrtb2/ctv_auction.go | 151 +- endpoints/openrtb2/ctv_auction_test.go | 230 ++- errortypes/code.go | 1 + errortypes/errortypes.go | 16 + exchange/adapter_builders.go | 2 + exchange/bidder.go | 6 + exchange/events.go | 4 +- exchange/events_test.go | 3 +- exchange/exchange.go | 86 ++ exchange/exchange_test.go | 557 ++++++++ go.mod | 4 +- go.sum | 2 + openrtb_ext/bid.go | 1 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_vastbidder.go | 18 + openrtb_ext/response.go | 1 + static/bidder-info/vastbidder.yaml | 9 + static/bidder-params/vastbidder.json | 27 + usersync/usersyncers/syncer_test.go | 1 + 38 files changed, 6058 insertions(+), 22 deletions(-) create mode 100644 adapters/vastbidder/bidder_macro.go create mode 100644 adapters/vastbidder/bidder_macro_test.go create mode 100644 adapters/vastbidder/constant.go create mode 100644 adapters/vastbidder/ibidder_macro.go create mode 100644 adapters/vastbidder/itag_response_handler.go create mode 100644 adapters/vastbidder/macro_processor.go create mode 100644 adapters/vastbidder/macro_processor_test.go create mode 100644 adapters/vastbidder/mapper.go create mode 100644 adapters/vastbidder/sample_spotx_macro.go.bak create mode 100644 adapters/vastbidder/tagbidder.go create mode 100644 adapters/vastbidder/tagbidder_test.go create mode 100644 adapters/vastbidder/util.go create mode 100644 adapters/vastbidder/vast_tag_response_handler.go create mode 100644 adapters/vastbidder/vast_tag_response_handler_test.go create mode 100644 openrtb_ext/imp_vastbidder.go create mode 100644 static/bidder-info/vastbidder.yaml create mode 100644 static/bidder-params/vastbidder.json diff --git a/adapters/bidder.go b/adapters/bidder.go index 395d4bd2201..2ac0f835226 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -110,8 +110,14 @@ type ResponseData struct { Headers http.Header } +type BidRequestParams struct { + ImpIndex int + VASTTagIndex int +} + // RequestData packages together the fields needed to make an http.Request. type RequestData struct { + Params *BidRequestParams Method string Uri string Body []byte diff --git a/adapters/vastbidder/bidder_macro.go b/adapters/vastbidder/bidder_macro.go new file mode 100644 index 00000000000..1df6fe2f444 --- /dev/null +++ b/adapters/vastbidder/bidder_macro.go @@ -0,0 +1,1233 @@ +package vastbidder + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//BidderMacro default implementation +type BidderMacro struct { + IBidderMacro + + //Configuration Parameters + Conf *config.Adapter + + //OpenRTB Specific Parameters + Request *openrtb2.BidRequest + IsApp bool + HasGeo bool + Imp *openrtb2.Imp + Publisher *openrtb2.Publisher + Content *openrtb2.Content + + //Extensions + ImpBidderExt openrtb_ext.ExtImpVASTBidder + VASTTag *openrtb_ext.ExtImpVASTBidderTag + UserExt *openrtb_ext.ExtUser + RegsExt *openrtb_ext.ExtRegs + + //Impression level Request Headers + ImpReqHeaders http.Header +} + +//NewBidderMacro contains definition for all openrtb macro's +func NewBidderMacro() IBidderMacro { + obj := &BidderMacro{} + obj.IBidderMacro = obj + return obj +} + +func (tag *BidderMacro) init() { + if nil != tag.Request.App { + tag.IsApp = true + tag.Publisher = tag.Request.App.Publisher + tag.Content = tag.Request.App.Content + } else { + tag.Publisher = tag.Request.Site.Publisher + tag.Content = tag.Request.Site.Content + } + tag.HasGeo = nil != tag.Request.Device && nil != tag.Request.Device.Geo + + //Read User Extensions + if nil != tag.Request.User && nil != tag.Request.User.Ext { + var ext openrtb_ext.ExtUser + err := json.Unmarshal(tag.Request.User.Ext, &ext) + if nil == err { + tag.UserExt = &ext + } + } + + //Read Regs Extensions + if nil != tag.Request.Regs && nil != tag.Request.Regs.Ext { + var ext openrtb_ext.ExtRegs + err := json.Unmarshal(tag.Request.Regs.Ext, &ext) + if nil == err { + tag.RegsExt = &ext + } + } +} + +//InitBidRequest will initialise BidRequest +func (tag *BidderMacro) InitBidRequest(request *openrtb2.BidRequest) { + tag.Request = request + tag.init() +} + +//LoadImpression will set current imp +func (tag *BidderMacro) LoadImpression(imp *openrtb2.Imp) (*openrtb_ext.ExtImpVASTBidder, error) { + tag.Imp = imp + + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, err + } + + tag.ImpBidderExt = openrtb_ext.ExtImpVASTBidder{} + if err := json.Unmarshal(bidderExt.Bidder, &tag.ImpBidderExt); err != nil { + return nil, err + } + return &tag.ImpBidderExt, nil +} + +//LoadVASTTag will set current VAST Tag details in bidder keys +func (tag *BidderMacro) LoadVASTTag(vastTag *openrtb_ext.ExtImpVASTBidderTag) { + tag.VASTTag = vastTag +} + +//GetBidderKeys will set bidder level keys +func (tag *BidderMacro) GetBidderKeys() map[string]string { + var keys map[string]string + //Adding VAST Tag Bidder Parameters + keys = NormalizeJSON(tag.VASTTag.Params) + + //Adding VAST Tag Standard Params + keys["dur"] = strconv.Itoa(tag.VASTTag.Duration) + + //Adding Headers as Custom Macros + + //Adding Cookies as Custom Macros + + //Adding Default Empty for standard keys + for i := range ParamKeys { + if _, ok := keys[ParamKeys[i]]; !ok { + keys[ParamKeys[i]] = "" + } + } + return keys +} + +//SetAdapterConfig will set Adapter config +func (tag *BidderMacro) SetAdapterConfig(conf *config.Adapter) { + tag.Conf = conf +} + +//GetURI get URL +func (tag *BidderMacro) GetURI() string { + + //check for URI at impression level + if nil != tag.VASTTag { + return tag.VASTTag.URL + } + + //check for URI at config level + return tag.Conf.Endpoint +} + +//GetHeaders returns list of custom request headers +//Override this method if your Vast bidder needs custom request headers +func (tag *BidderMacro) GetHeaders() http.Header { + return http.Header{} +} + +/********************* Request *********************/ + +//MacroTest contains definition for Test Parameter +func (tag *BidderMacro) MacroTest(key string) string { + if tag.Request.Test > 0 { + return strconv.Itoa(int(tag.Request.Test)) + } + return "" +} + +//MacroTimeout contains definition for Timeout Parameter +func (tag *BidderMacro) MacroTimeout(key string) string { + if tag.Request.TMax > 0 { + return strconv.FormatInt(tag.Request.TMax, intBase) + } + return "" +} + +//MacroWhitelistSeat contains definition for WhitelistSeat Parameter +func (tag *BidderMacro) MacroWhitelistSeat(key string) string { + return strings.Join(tag.Request.WSeat, comma) +} + +//MacroWhitelistLang contains definition for WhitelistLang Parameter +func (tag *BidderMacro) MacroWhitelistLang(key string) string { + return strings.Join(tag.Request.WLang, comma) +} + +//MacroBlockedSeat contains definition for Blockedseat Parameter +func (tag *BidderMacro) MacroBlockedSeat(key string) string { + return strings.Join(tag.Request.BSeat, comma) +} + +//MacroCurrency contains definition for Currency Parameter +func (tag *BidderMacro) MacroCurrency(key string) string { + return strings.Join(tag.Request.Cur, comma) +} + +//MacroBlockedCategory contains definition for BlockedCategory Parameter +func (tag *BidderMacro) MacroBlockedCategory(key string) string { + return strings.Join(tag.Request.BCat, comma) +} + +//MacroBlockedAdvertiser contains definition for BlockedAdvertiser Parameter +func (tag *BidderMacro) MacroBlockedAdvertiser(key string) string { + return strings.Join(tag.Request.BAdv, comma) +} + +//MacroBlockedApp contains definition for BlockedApp Parameter +func (tag *BidderMacro) MacroBlockedApp(key string) string { + return strings.Join(tag.Request.BApp, comma) +} + +/********************* Source *********************/ + +//MacroFD contains definition for FD Parameter +func (tag *BidderMacro) MacroFD(key string) string { + if nil != tag.Request.Source { + return strconv.Itoa(int(tag.Request.Source.FD)) + } + return "" +} + +//MacroTransactionID contains definition for TransactionID Parameter +func (tag *BidderMacro) MacroTransactionID(key string) string { + if nil != tag.Request.Source { + return tag.Request.Source.TID + } + return "" +} + +//MacroPaymentIDChain contains definition for PaymentIDChain Parameter +func (tag *BidderMacro) MacroPaymentIDChain(key string) string { + if nil != tag.Request.Source { + return tag.Request.Source.PChain + } + return "" +} + +/********************* Regs *********************/ + +//MacroCoppa contains definition for Coppa Parameter +func (tag *BidderMacro) MacroCoppa(key string) string { + if nil != tag.Request.Regs { + return strconv.Itoa(int(tag.Request.Regs.COPPA)) + } + return "" +} + +/********************* Impression *********************/ + +//MacroDisplayManager contains definition for DisplayManager Parameter +func (tag *BidderMacro) MacroDisplayManager(key string) string { + return tag.Imp.DisplayManager +} + +//MacroDisplayManagerVersion contains definition for DisplayManagerVersion Parameter +func (tag *BidderMacro) MacroDisplayManagerVersion(key string) string { + return tag.Imp.DisplayManagerVer +} + +//MacroInterstitial contains definition for Interstitial Parameter +func (tag *BidderMacro) MacroInterstitial(key string) string { + if tag.Imp.Instl > 0 { + return strconv.Itoa(int(tag.Imp.Instl)) + } + return "" +} + +//MacroTagID contains definition for TagID Parameter +func (tag *BidderMacro) MacroTagID(key string) string { + return tag.Imp.TagID +} + +//MacroBidFloor contains definition for BidFloor Parameter +func (tag *BidderMacro) MacroBidFloor(key string) string { + if tag.Imp.BidFloor > 0 { + return fmt.Sprintf("%g", tag.Imp.BidFloor) + } + return "" +} + +//MacroBidFloorCurrency contains definition for BidFloorCurrency Parameter +func (tag *BidderMacro) MacroBidFloorCurrency(key string) string { + return tag.Imp.BidFloorCur +} + +//MacroSecure contains definition for Secure Parameter +func (tag *BidderMacro) MacroSecure(key string) string { + if nil != tag.Imp.Secure { + return strconv.Itoa(int(*tag.Imp.Secure)) + } + return "" +} + +//MacroPMP contains definition for PMP Parameter +func (tag *BidderMacro) MacroPMP(key string) string { + if nil != tag.Imp.PMP { + data, _ := json.Marshal(tag.Imp.PMP) + return string(data) + } + return "" +} + +/********************* Video *********************/ + +//MacroVideoMIMES contains definition for VideoMIMES Parameter +func (tag *BidderMacro) MacroVideoMIMES(key string) string { + if nil != tag.Imp.Video { + return strings.Join(tag.Imp.Video.MIMEs, comma) + } + return "" +} + +//MacroVideoMinimumDuration contains definition for VideoMinimumDuration Parameter +func (tag *BidderMacro) MacroVideoMinimumDuration(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MinDuration > 0 { + return strconv.FormatInt(tag.Imp.Video.MinDuration, intBase) + } + return "" +} + +//MacroVideoMaximumDuration contains definition for VideoMaximumDuration Parameter +func (tag *BidderMacro) MacroVideoMaximumDuration(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxDuration > 0 { + return strconv.FormatInt(tag.Imp.Video.MaxDuration, intBase) + } + return "" +} + +//MacroVideoProtocols contains definition for VideoProtocols Parameter +func (tag *BidderMacro) MacroVideoProtocols(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.Protocols + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoPlayerWidth contains definition for VideoPlayerWidth Parameter +func (tag *BidderMacro) MacroVideoPlayerWidth(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.W > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.W), intBase) + } + return "" +} + +//MacroVideoPlayerHeight contains definition for VideoPlayerHeight Parameter +func (tag *BidderMacro) MacroVideoPlayerHeight(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.H > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.H), intBase) + } + return "" +} + +//MacroVideoStartDelay contains definition for VideoStartDelay Parameter +func (tag *BidderMacro) MacroVideoStartDelay(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.StartDelay { + return strconv.FormatInt(int64(*tag.Imp.Video.StartDelay), intBase) + } + return "" +} + +//MacroVideoPlacement contains definition for VideoPlacement Parameter +func (tag *BidderMacro) MacroVideoPlacement(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Placement > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Placement), intBase) + } + return "" +} + +//MacroVideoLinearity contains definition for VideoLinearity Parameter +func (tag *BidderMacro) MacroVideoLinearity(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Linearity > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Linearity), intBase) + } + return "" +} + +//MacroVideoSkip contains definition for VideoSkip Parameter +func (tag *BidderMacro) MacroVideoSkip(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.Skip { + return strconv.FormatInt(int64(*tag.Imp.Video.Skip), intBase) + } + return "" +} + +//MacroVideoSkipMinimum contains definition for VideoSkipMinimum Parameter +func (tag *BidderMacro) MacroVideoSkipMinimum(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.SkipMin > 0 { + return strconv.FormatInt(tag.Imp.Video.SkipMin, intBase) + } + return "" +} + +//MacroVideoSkipAfter contains definition for VideoSkipAfter Parameter +func (tag *BidderMacro) MacroVideoSkipAfter(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.SkipAfter > 0 { + return strconv.FormatInt(tag.Imp.Video.SkipAfter, intBase) + } + return "" +} + +//MacroVideoSequence contains definition for VideoSequence Parameter +func (tag *BidderMacro) MacroVideoSequence(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Sequence > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Sequence), intBase) + } + return "" +} + +//MacroVideoBlockedAttribute contains definition for VideoBlockedAttribute Parameter +func (tag *BidderMacro) MacroVideoBlockedAttribute(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.BAttr + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoMaximumExtended contains definition for VideoMaximumExtended Parameter +func (tag *BidderMacro) MacroVideoMaximumExtended(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxExtended > 0 { + return strconv.FormatInt(tag.Imp.Video.MaxExtended, intBase) + } + return "" +} + +//MacroVideoMinimumBitRate contains definition for VideoMinimumBitRate Parameter +func (tag *BidderMacro) MacroVideoMinimumBitRate(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MinBitRate > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.MinBitRate), intBase) + } + return "" +} + +//MacroVideoMaximumBitRate contains definition for VideoMaximumBitRate Parameter +func (tag *BidderMacro) MacroVideoMaximumBitRate(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxBitRate > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.MaxBitRate), intBase) + } + return "" +} + +//MacroVideoBoxing contains definition for VideoBoxing Parameter +func (tag *BidderMacro) MacroVideoBoxing(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.BoxingAllowed > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.BoxingAllowed), intBase) + } + return "" +} + +//MacroVideoPlaybackMethod contains definition for VideoPlaybackMethod Parameter +func (tag *BidderMacro) MacroVideoPlaybackMethod(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.PlaybackMethod + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoDelivery contains definition for VideoDelivery Parameter +func (tag *BidderMacro) MacroVideoDelivery(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.Delivery + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoPosition contains definition for VideoPosition Parameter +func (tag *BidderMacro) MacroVideoPosition(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.Pos { + return strconv.FormatInt(int64(*tag.Imp.Video.Pos), intBase) + } + return "" +} + +//MacroVideoAPI contains definition for VideoAPI Parameter +func (tag *BidderMacro) MacroVideoAPI(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.API + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +/********************* Site *********************/ + +//MacroSiteID contains definition for SiteID Parameter +func (tag *BidderMacro) MacroSiteID(key string) string { + if !tag.IsApp { + return tag.Request.Site.ID + } + return "" +} + +//MacroSiteName contains definition for SiteName Parameter +func (tag *BidderMacro) MacroSiteName(key string) string { + if !tag.IsApp { + return tag.Request.Site.Name + } + return "" +} + +//MacroSitePage contains definition for SitePage Parameter +func (tag *BidderMacro) MacroSitePage(key string) string { + if !tag.IsApp && nil != tag.Request && nil != tag.Request.Site { + return tag.Request.Site.Page + } + return "" +} + +//MacroSiteReferrer contains definition for SiteReferrer Parameter +func (tag *BidderMacro) MacroSiteReferrer(key string) string { + if !tag.IsApp { + return tag.Request.Site.Ref + } + return "" +} + +//MacroSiteSearch contains definition for SiteSearch Parameter +func (tag *BidderMacro) MacroSiteSearch(key string) string { + if !tag.IsApp { + return tag.Request.Site.Search + } + return "" +} + +//MacroSiteMobile contains definition for SiteMobile Parameter +func (tag *BidderMacro) MacroSiteMobile(key string) string { + if !tag.IsApp && tag.Request.Site.Mobile > 0 { + return strconv.FormatInt(int64(tag.Request.Site.Mobile), intBase) + } + return "" +} + +/********************* App *********************/ + +//MacroAppID contains definition for AppID Parameter +func (tag *BidderMacro) MacroAppID(key string) string { + if tag.IsApp { + return tag.Request.App.ID + } + return "" +} + +//MacroAppName contains definition for AppName Parameter +func (tag *BidderMacro) MacroAppName(key string) string { + if tag.IsApp { + return tag.Request.App.Name + } + return "" +} + +//MacroAppBundle contains definition for AppBundle Parameter +func (tag *BidderMacro) MacroAppBundle(key string) string { + if tag.IsApp { + return tag.Request.App.Bundle + } + return "" +} + +//MacroAppStoreURL contains definition for AppStoreURL Parameter +func (tag *BidderMacro) MacroAppStoreURL(key string) string { + if tag.IsApp { + return tag.Request.App.StoreURL + } + return "" +} + +//MacroAppVersion contains definition for AppVersion Parameter +func (tag *BidderMacro) MacroAppVersion(key string) string { + if tag.IsApp { + return tag.Request.App.Ver + } + return "" +} + +//MacroAppPaid contains definition for AppPaid Parameter +func (tag *BidderMacro) MacroAppPaid(key string) string { + if tag.IsApp && tag.Request.App.Paid != 0 { + return strconv.FormatInt(int64(tag.Request.App.Paid), intBase) + } + return "" +} + +/********************* Site/App Common *********************/ + +//MacroCategory contains definition for Category Parameter +func (tag *BidderMacro) MacroCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.Cat, comma) + } + return strings.Join(tag.Request.Site.Cat, comma) +} + +//MacroDomain contains definition for Domain Parameter +func (tag *BidderMacro) MacroDomain(key string) string { + if tag.IsApp { + return tag.Request.App.Domain + } + return tag.Request.Site.Domain +} + +//MacroSectionCategory contains definition for SectionCategory Parameter +func (tag *BidderMacro) MacroSectionCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.SectionCat, comma) + } + return strings.Join(tag.Request.Site.SectionCat, comma) +} + +//MacroPageCategory contains definition for PageCategory Parameter +func (tag *BidderMacro) MacroPageCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.PageCat, comma) + } + return strings.Join(tag.Request.Site.PageCat, comma) +} + +//MacroPrivacyPolicy contains definition for PrivacyPolicy Parameter +func (tag *BidderMacro) MacroPrivacyPolicy(key string) string { + var value int8 = 0 + if tag.IsApp { + value = tag.Request.App.PrivacyPolicy + } else { + value = tag.Request.Site.PrivacyPolicy + } + if value > 0 { + return strconv.FormatInt(int64(value), intBase) + } + return "" +} + +//MacroKeywords contains definition for Keywords Parameter +func (tag *BidderMacro) MacroKeywords(key string) string { + if tag.IsApp { + return tag.Request.App.Keywords + } + return tag.Request.Site.Keywords +} + +/********************* Publisher *********************/ + +//MacroPubID contains definition for PubID Parameter +func (tag *BidderMacro) MacroPubID(key string) string { + if nil != tag.Publisher { + return tag.Publisher.ID + } + return "" +} + +//MacroPubName contains definition for PubName Parameter +func (tag *BidderMacro) MacroPubName(key string) string { + if nil != tag.Publisher { + return tag.Publisher.Name + } + return "" +} + +//MacroPubDomain contains definition for PubDomain Parameter +func (tag *BidderMacro) MacroPubDomain(key string) string { + if nil != tag.Publisher { + return tag.Publisher.Domain + } + return "" +} + +/********************* Content *********************/ + +//MacroContentID contains definition for ContentID Parameter +func (tag *BidderMacro) MacroContentID(key string) string { + if nil != tag.Content { + return tag.Content.ID + } + return "" +} + +//MacroContentEpisode contains definition for ContentEpisode Parameter +func (tag *BidderMacro) MacroContentEpisode(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Episode), intBase) + } + return "" +} + +//MacroContentTitle contains definition for ContentTitle Parameter +func (tag *BidderMacro) MacroContentTitle(key string) string { + if nil != tag.Content { + return tag.Content.Title + } + return "" +} + +//MacroContentSeries contains definition for ContentSeries Parameter +func (tag *BidderMacro) MacroContentSeries(key string) string { + if nil != tag.Content { + return tag.Content.Series + } + return "" +} + +//MacroContentSeason contains definition for ContentSeason Parameter +func (tag *BidderMacro) MacroContentSeason(key string) string { + if nil != tag.Content { + return tag.Content.Season + } + return "" +} + +//MacroContentArtist contains definition for ContentArtist Parameter +func (tag *BidderMacro) MacroContentArtist(key string) string { + if nil != tag.Content { + return tag.Content.Artist + } + return "" +} + +//MacroContentGenre contains definition for ContentGenre Parameter +func (tag *BidderMacro) MacroContentGenre(key string) string { + if nil != tag.Content { + return tag.Content.Genre + } + return "" +} + +//MacroContentAlbum contains definition for ContentAlbum Parameter +func (tag *BidderMacro) MacroContentAlbum(key string) string { + if nil != tag.Content { + return tag.Content.Album + } + return "" +} + +//MacroContentISrc contains definition for ContentISrc Parameter +func (tag *BidderMacro) MacroContentISrc(key string) string { + if nil != tag.Content { + return tag.Content.ISRC + } + return "" +} + +//MacroContentURL contains definition for ContentURL Parameter +func (tag *BidderMacro) MacroContentURL(key string) string { + if nil != tag.Content { + return tag.Content.URL + } + return "" +} + +//MacroContentCategory contains definition for ContentCategory Parameter +func (tag *BidderMacro) MacroContentCategory(key string) string { + if nil != tag.Content { + return strings.Join(tag.Content.Cat, comma) + } + return "" +} + +//MacroContentProductionQuality contains definition for ContentProductionQuality Parameter +func (tag *BidderMacro) MacroContentProductionQuality(key string) string { + if nil != tag.Content && nil != tag.Content.ProdQ { + return strconv.FormatInt(int64(*tag.Content.ProdQ), intBase) + } + return "" +} + +//MacroContentVideoQuality contains definition for ContentVideoQuality Parameter +func (tag *BidderMacro) MacroContentVideoQuality(key string) string { + if nil != tag.Content && nil != tag.Content.VideoQuality { + return strconv.FormatInt(int64(*tag.Content.VideoQuality), intBase) + } + return "" +} + +//MacroContentContext contains definition for ContentContext Parameter +func (tag *BidderMacro) MacroContentContext(key string) string { + if nil != tag.Content && tag.Content.Context > 0 { + return strconv.FormatInt(int64(tag.Content.Context), intBase) + } + return "" +} + +//MacroContentContentRating contains definition for ContentContentRating Parameter +func (tag *BidderMacro) MacroContentContentRating(key string) string { + if nil != tag.Content { + return tag.Content.ContentRating + } + return "" +} + +//MacroContentUserRating contains definition for ContentUserRating Parameter +func (tag *BidderMacro) MacroContentUserRating(key string) string { + if nil != tag.Content { + return tag.Content.UserRating + } + return "" +} + +//MacroContentQAGMediaRating contains definition for ContentQAGMediaRating Parameter +func (tag *BidderMacro) MacroContentQAGMediaRating(key string) string { + if nil != tag.Content && tag.Content.QAGMediaRating > 0 { + return strconv.FormatInt(int64(tag.Content.QAGMediaRating), intBase) + } + return "" +} + +//MacroContentKeywords contains definition for ContentKeywords Parameter +func (tag *BidderMacro) MacroContentKeywords(key string) string { + if nil != tag.Content { + return tag.Content.Keywords + } + return "" +} + +//MacroContentLiveStream contains definition for ContentLiveStream Parameter +func (tag *BidderMacro) MacroContentLiveStream(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.LiveStream), intBase) + } + return "" +} + +//MacroContentSourceRelationship contains definition for ContentSourceRelationship Parameter +func (tag *BidderMacro) MacroContentSourceRelationship(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.SourceRelationship), intBase) + } + return "" +} + +//MacroContentLength contains definition for ContentLength Parameter +func (tag *BidderMacro) MacroContentLength(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Len), intBase) + } + return "" +} + +//MacroContentLanguage contains definition for ContentLanguage Parameter +func (tag *BidderMacro) MacroContentLanguage(key string) string { + if nil != tag.Content { + return tag.Content.Language + } + return "" +} + +//MacroContentEmbeddable contains definition for ContentEmbeddable Parameter +func (tag *BidderMacro) MacroContentEmbeddable(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Embeddable), intBase) + } + return "" +} + +/********************* Producer *********************/ + +//MacroProducerID contains definition for ProducerID Parameter +func (tag *BidderMacro) MacroProducerID(key string) string { + if nil != tag.Content && nil != tag.Content.Producer { + return tag.Content.Producer.ID + } + return "" +} + +//MacroProducerName contains definition for ProducerName Parameter +func (tag *BidderMacro) MacroProducerName(key string) string { + if nil != tag.Content && nil != tag.Content.Producer { + return tag.Content.Producer.Name + } + return "" +} + +/********************* Device *********************/ + +//MacroUserAgent contains definition for UserAgent Parameter +func (tag *BidderMacro) MacroUserAgent(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + return tag.Request.Device.UA + } + return "" +} + +//MacroDNT contains definition for DNT Parameter +func (tag *BidderMacro) MacroDNT(key string) string { + if nil != tag.Request.Device && nil != tag.Request.Device.DNT { + return strconv.FormatInt(int64(*tag.Request.Device.DNT), intBase) + } + return "" +} + +//MacroLMT contains definition for LMT Parameter +func (tag *BidderMacro) MacroLMT(key string) string { + if nil != tag.Request.Device && nil != tag.Request.Device.Lmt { + return strconv.FormatInt(int64(*tag.Request.Device.Lmt), intBase) + } + return "" +} + +//MacroIP contains definition for IP Parameter +func (tag *BidderMacro) MacroIP(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + if len(tag.Request.Device.IP) > 0 { + return tag.Request.Device.IP + } else if len(tag.Request.Device.IPv6) > 0 { + return tag.Request.Device.IPv6 + } + } + return "" +} + +//MacroDeviceType contains definition for DeviceType Parameter +func (tag *BidderMacro) MacroDeviceType(key string) string { + if nil != tag.Request.Device && tag.Request.Device.DeviceType > 0 { + return strconv.FormatInt(int64(tag.Request.Device.DeviceType), intBase) + } + return "" +} + +//MacroMake contains definition for Make Parameter +func (tag *BidderMacro) MacroMake(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.Make + } + return "" +} + +//MacroModel contains definition for Model Parameter +func (tag *BidderMacro) MacroModel(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.Model + } + return "" +} + +//MacroDeviceOS contains definition for DeviceOS Parameter +func (tag *BidderMacro) MacroDeviceOS(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.OS + } + return "" +} + +//MacroDeviceOSVersion contains definition for DeviceOSVersion Parameter +func (tag *BidderMacro) MacroDeviceOSVersion(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.OSV + } + return "" +} + +//MacroDeviceWidth contains definition for DeviceWidth Parameter +func (tag *BidderMacro) MacroDeviceWidth(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.W), intBase) + } + return "" +} + +//MacroDeviceHeight contains definition for DeviceHeight Parameter +func (tag *BidderMacro) MacroDeviceHeight(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.H), intBase) + } + return "" +} + +//MacroDeviceJS contains definition for DeviceJS Parameter +func (tag *BidderMacro) MacroDeviceJS(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.JS), intBase) + } + return "" +} + +//MacroDeviceLanguage contains definition for DeviceLanguage Parameter +func (tag *BidderMacro) MacroDeviceLanguage(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + return tag.Request.Device.Language + } + return "" +} + +//MacroDeviceIFA contains definition for DeviceIFA Parameter +func (tag *BidderMacro) MacroDeviceIFA(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.IFA + } + return "" +} + +//MacroDeviceDIDSHA1 contains definition for DeviceDIDSHA1 Parameter +func (tag *BidderMacro) MacroDeviceDIDSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DIDSHA1 + } + return "" +} + +//MacroDeviceDIDMD5 contains definition for DeviceDIDMD5 Parameter +func (tag *BidderMacro) MacroDeviceDIDMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DIDMD5 + } + return "" +} + +//MacroDeviceDPIDSHA1 contains definition for DeviceDPIDSHA1 Parameter +func (tag *BidderMacro) MacroDeviceDPIDSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DPIDSHA1 + } + return "" +} + +//MacroDeviceDPIDMD5 contains definition for DeviceDPIDMD5 Parameter +func (tag *BidderMacro) MacroDeviceDPIDMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DPIDMD5 + } + return "" +} + +//MacroDeviceMACSHA1 contains definition for DeviceMACSHA1 Parameter +func (tag *BidderMacro) MacroDeviceMACSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.MACSHA1 + } + return "" +} + +//MacroDeviceMACMD5 contains definition for DeviceMACMD5 Parameter +func (tag *BidderMacro) MacroDeviceMACMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.MACMD5 + } + return "" +} + +/********************* Geo *********************/ + +//MacroLatitude contains definition for Latitude Parameter +func (tag *BidderMacro) MacroLatitude(key string) string { + if tag.HasGeo { + return fmt.Sprintf("%g", tag.Request.Device.Geo.Lat) + } + return "" +} + +//MacroLongitude contains definition for Longitude Parameter +func (tag *BidderMacro) MacroLongitude(key string) string { + if tag.HasGeo { + return fmt.Sprintf("%g", tag.Request.Device.Geo.Lon) + } + return "" +} + +//MacroCountry contains definition for Country Parameter +func (tag *BidderMacro) MacroCountry(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.Country + } + return "" +} + +//MacroRegion contains definition for Region Parameter +func (tag *BidderMacro) MacroRegion(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.Region + } + return "" +} + +//MacroCity contains definition for City Parameter +func (tag *BidderMacro) MacroCity(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.City + } + return "" +} + +//MacroZip contains definition for Zip Parameter +func (tag *BidderMacro) MacroZip(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.ZIP + } + return "" +} + +//MacroUTCOffset contains definition for UTCOffset Parameter +func (tag *BidderMacro) MacroUTCOffset(key string) string { + if tag.HasGeo { + return strconv.FormatInt(tag.Request.Device.Geo.UTCOffset, intBase) + } + return "" +} + +/********************* User *********************/ + +//MacroUserID contains definition for UserID Parameter +func (tag *BidderMacro) MacroUserID(key string) string { + if nil != tag.Request.User { + return tag.Request.User.ID + } + return "" +} + +//MacroYearOfBirth contains definition for YearOfBirth Parameter +func (tag *BidderMacro) MacroYearOfBirth(key string) string { + if nil != tag.Request.User && tag.Request.User.Yob > 0 { + return strconv.FormatInt(tag.Request.User.Yob, intBase) + } + return "" +} + +//MacroGender contains definition for Gender Parameter +func (tag *BidderMacro) MacroGender(key string) string { + if nil != tag.Request.User { + return tag.Request.User.Gender + } + return "" +} + +/********************* Extension *********************/ + +//MacroGDPRConsent contains definition for GDPRConsent Parameter +func (tag *BidderMacro) MacroGDPRConsent(key string) string { + if nil != tag.UserExt { + return tag.UserExt.Consent + } + return "" +} + +//MacroGDPR contains definition for GDPR Parameter +func (tag *BidderMacro) MacroGDPR(key string) string { + if nil != tag.RegsExt && nil != tag.RegsExt.GDPR { + return strconv.FormatInt(int64(*tag.RegsExt.GDPR), intBase) + } + return "" +} + +//MacroUSPrivacy contains definition for USPrivacy Parameter +func (tag *BidderMacro) MacroUSPrivacy(key string) string { + if nil != tag.RegsExt { + return tag.RegsExt.USPrivacy + } + return "" +} + +/********************* Additional *********************/ + +//MacroCacheBuster contains definition for CacheBuster Parameter +func (tag *BidderMacro) MacroCacheBuster(key string) string { + //change implementation + return strconv.FormatInt(time.Now().UnixNano(), intBase) +} + +/********************* Request Headers *********************/ + +// setDefaultHeaders sets following default headers based on VAST protocol version +// X-device-IP; end users IP address, per VAST 4.x +// X-Forwarded-For; end users IP address, prior VAST versions +// X-Device-User-Agent; End users user agent, per VAST 4.x +// User-Agent; End users user agent, prior VAST versions +// X-Device-Referer; Referer value from the original request, per VAST 4.x +// X-device-Accept-Language, Accept-language value from the original request, per VAST 4.x +func setDefaultHeaders(tag *BidderMacro) { + // openrtb2. auction.go setDeviceImplicitly + // already populates OpenRTB bid request based on http request headers + // reusing the same information to set these headers via Macro* methods + headers := http.Header{} + ip := tag.IBidderMacro.MacroIP("") + userAgent := tag.IBidderMacro.MacroUserAgent("") + referer := tag.IBidderMacro.MacroSitePage("") + language := tag.IBidderMacro.MacroDeviceLanguage("") + + // 1 - vast 1 - 3 expected, 2 - vast 4 expected + expectedVastTags := 0 + if nil != tag.Imp && nil != tag.Imp.Video && nil != tag.Imp.Video.Protocols && len(tag.Imp.Video.Protocols) > 0 { + for _, protocol := range tag.Imp.Video.Protocols { + if protocol == openrtb2.ProtocolVAST40 || protocol == openrtb2.ProtocolVAST40Wrapper { + expectedVastTags |= 1 << 1 + } + if protocol <= openrtb2.ProtocolVAST30Wrapper { + expectedVastTags |= 1 << 0 + } + } + } else { + // not able to detect protocols. set all headers + expectedVastTags = 3 + } + + if expectedVastTags == 1 || expectedVastTags == 3 { + // vast prior to version 3 headers + setHeaders(headers, "X-Forwarded-For", ip) + setHeaders(headers, "User-Agent", userAgent) + } + + if expectedVastTags == 2 || expectedVastTags == 3 { + // vast 4 specific headers + setHeaders(headers, "X-device-Ip", ip) + setHeaders(headers, "X-Device-User-Agent", userAgent) + setHeaders(headers, "X-Device-Referer", referer) + setHeaders(headers, "X-Device-Accept-Language", language) + } + tag.ImpReqHeaders = headers +} + +func setHeaders(headers http.Header, key, value string) { + if "" != value { + headers.Set(key, value) + } +} + +//getAllHeaders combines default and custom headers and returns common list +//It internally calls GetHeaders() method for obtaining list of custom headers +func (tag *BidderMacro) getAllHeaders() http.Header { + setDefaultHeaders(tag) + customHeaders := tag.IBidderMacro.GetHeaders() + if nil != customHeaders { + for k, v := range customHeaders { + // custom header may contains default header key with value + // in such case custom value will be prefered + if nil != v && len(v) > 0 { + tag.ImpReqHeaders.Set(k, v[0]) + for i := 1; i < len(v); i++ { + tag.ImpReqHeaders.Add(k, v[i]) + } + } + } + } + return tag.ImpReqHeaders +} diff --git a/adapters/vastbidder/bidder_macro_test.go b/adapters/vastbidder/bidder_macro_test.go new file mode 100644 index 00000000000..0e6afa74cf4 --- /dev/null +++ b/adapters/vastbidder/bidder_macro_test.go @@ -0,0 +1,1258 @@ +package vastbidder + +import ( + "fmt" + "net/http" + "testing" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/stretchr/testify/assert" +) + +//TestSetDefaultHeaders verifies SetDefaultHeaders +func TestSetDefaultHeaders(t *testing.T) { + type args struct { + req *openrtb2.BidRequest + } + type want struct { + headers http.Header + } + tests := []struct { + name string + args args + want want + }{ + { + name: "check all default headers", + args: args{req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }}, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + { + name: "nil bid request", + args: args{req: nil}, + want: want{ + headers: http.Header{}, + }, + }, + { + name: "no headers set", + args: args{req: &openrtb2.BidRequest{}}, + want: want{ + headers: http.Header{}, + }, + }, { + name: "vast 4 protocol", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + openrtb2.ProtocolDAAST10, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, { + name: "< vast 4", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST20, + openrtb2.ProtocolDAAST10, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Forwarded-For": []string{"1.1.1.1"}, + "User-Agent": []string{"user-agent"}, + }, + }, + }, { + name: "vast 4.0 and 4.0 wrapper", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + openrtb2.ProtocolVAST40Wrapper, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + { + name: "vast 2.0 and 4.0", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + openrtb2.ProtocolVAST20Wrapper, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tag := new(BidderMacro) + tag.IBidderMacro = tag + tag.IsApp = false + tag.Request = tt.args.req + if nil != tt.args.req && nil != tt.args.req.Imp && len(tt.args.req.Imp) > 0 { + tag.Imp = &tt.args.req.Imp[0] + } + setDefaultHeaders(tag) + assert.Equal(t, tt.want.headers, tag.ImpReqHeaders) + }) + } +} + +//TestGetAllHeaders verifies default and custom headers are returned +func TestGetAllHeaders(t *testing.T) { + type args struct { + req *openrtb2.BidRequest + myBidder IBidderMacro + } + type want struct { + headers http.Header + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "Default and custom headers check", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }, + myBidder: newMyVastBidderMacro(map[string]string{ + "my-custom-header": "some-value", + }), + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"some-value"}, + }, + }, + }, + { + name: "override default header value", + args: args{ + req: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Page: "http://test.com/", // default header value + }, + }, + myBidder: newMyVastBidderMacro(map[string]string{ + "X-Device-Referer": "my-custom-value", + }), + }, + want: want{ + headers: http.Header{ + // http://test.com/ is not expected here as value + "X-Device-Referer": []string{"my-custom-value"}, + }, + }, + }, + { + name: "no custom headers", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }, + myBidder: newMyVastBidderMacro(nil), // nil - no custom headers + }, + want: want{ + headers: http.Header{ // expect default headers + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tag := tt.args.myBidder + tag.(*myVastBidderMacro).Request = tt.args.req + allHeaders := tag.getAllHeaders() + assert.Equal(t, tt.want.headers, allHeaders) + }) + } +} + +type myVastBidderMacro struct { + *BidderMacro + customHeaders map[string]string +} + +func newMyVastBidderMacro(customHeaders map[string]string) IBidderMacro { + obj := &myVastBidderMacro{ + BidderMacro: &BidderMacro{}, + customHeaders: customHeaders, + } + obj.IBidderMacro = obj + return obj +} + +func (tag *myVastBidderMacro) GetHeaders() http.Header { + if nil == tag.customHeaders { + return nil + } + h := http.Header{} + for k, v := range tag.customHeaders { + h.Set(k, v) + } + return h +} + +type testBidderMacro struct { + *BidderMacro +} + +func (tag *testBidderMacro) MacroCacheBuster(key string) string { + return `cachebuster` +} + +func newTestBidderMacro() IBidderMacro { + obj := &testBidderMacro{ + BidderMacro: &BidderMacro{}, + } + obj.IBidderMacro = obj + return obj +} + +func TestBidderMacro_MacroTest(t *testing.T) { + type args struct { + tag IBidderMacro + conf *config.Adapter + bidRequest *openrtb2.BidRequest + } + tests := []struct { + name string + args args + macros map[string]string + }{ + { + name: `App:EmptyBasicRequest`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{}, + }, + }, + App: &openrtb2.App{ + Publisher: &openrtb2.Publisher{}, + }, + }, + }, + macros: map[string]string{ + MacroTest: ``, + MacroTimeout: ``, + MacroWhitelistSeat: ``, + MacroWhitelistLang: ``, + MacroBlockedSeat: ``, + MacroCurrency: ``, + MacroBlockedCategory: ``, + MacroBlockedAdvertiser: ``, + MacroBlockedApp: ``, + MacroFD: ``, + MacroTransactionID: ``, + MacroPaymentIDChain: ``, + MacroCoppa: ``, + MacroDisplayManager: ``, + MacroDisplayManagerVersion: ``, + MacroInterstitial: ``, + MacroTagID: ``, + MacroBidFloor: ``, + MacroBidFloorCurrency: ``, + MacroSecure: ``, + MacroPMP: ``, + MacroVideoMIMES: ``, + MacroVideoMinimumDuration: ``, + MacroVideoMaximumDuration: ``, + MacroVideoProtocols: ``, + MacroVideoPlayerWidth: ``, + MacroVideoPlayerHeight: ``, + MacroVideoStartDelay: ``, + MacroVideoPlacement: ``, + MacroVideoLinearity: ``, + MacroVideoSkip: ``, + MacroVideoSkipMinimum: ``, + MacroVideoSkipAfter: ``, + MacroVideoSequence: ``, + MacroVideoBlockedAttribute: ``, + MacroVideoMaximumExtended: ``, + MacroVideoMinimumBitRate: ``, + MacroVideoMaximumBitRate: ``, + MacroVideoBoxing: ``, + MacroVideoPlaybackMethod: ``, + MacroVideoDelivery: ``, + MacroVideoPosition: ``, + MacroVideoAPI: ``, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: ``, + MacroDomain: ``, + MacroSectionCategory: ``, + MacroPageCategory: ``, + MacroPrivacyPolicy: ``, + MacroKeywords: ``, + MacroPubID: ``, + MacroPubName: ``, + MacroPubDomain: ``, + MacroContentID: ``, + MacroContentEpisode: ``, + MacroContentTitle: ``, + MacroContentSeries: ``, + MacroContentSeason: ``, + MacroContentArtist: ``, + MacroContentGenre: ``, + MacroContentAlbum: ``, + MacroContentISrc: ``, + MacroContentURL: ``, + MacroContentCategory: ``, + MacroContentProductionQuality: ``, + MacroContentVideoQuality: ``, + MacroContentContext: ``, + MacroContentContentRating: ``, + MacroContentUserRating: ``, + MacroContentQAGMediaRating: ``, + MacroContentKeywords: ``, + MacroContentLiveStream: ``, + MacroContentSourceRelationship: ``, + MacroContentLength: ``, + MacroContentLanguage: ``, + MacroContentEmbeddable: ``, + MacroProducerID: ``, + MacroProducerName: ``, + MacroUserAgent: ``, + MacroDNT: ``, + MacroLMT: ``, + MacroIP: ``, + MacroDeviceType: ``, + MacroMake: ``, + MacroModel: ``, + MacroDeviceOS: ``, + MacroDeviceOSVersion: ``, + MacroDeviceWidth: ``, + MacroDeviceHeight: ``, + MacroDeviceJS: ``, + MacroDeviceLanguage: ``, + MacroDeviceIFA: ``, + MacroDeviceDIDSHA1: ``, + MacroDeviceDIDMD5: ``, + MacroDeviceDPIDSHA1: ``, + MacroDeviceDPIDMD5: ``, + MacroDeviceMACSHA1: ``, + MacroDeviceMACMD5: ``, + MacroLatitude: ``, + MacroLongitude: ``, + MacroCountry: ``, + MacroRegion: ``, + MacroCity: ``, + MacroZip: ``, + MacroUTCOffset: ``, + MacroUserID: ``, + MacroYearOfBirth: ``, + MacroGender: ``, + MacroGDPRConsent: ``, + MacroGDPR: ``, + MacroUSPrivacy: ``, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `Site:EmptyBasicRequest`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{}, + }, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{}, + }, + }, + }, + macros: map[string]string{ + MacroTest: ``, + MacroTimeout: ``, + MacroWhitelistSeat: ``, + MacroWhitelistLang: ``, + MacroBlockedSeat: ``, + MacroCurrency: ``, + MacroBlockedCategory: ``, + MacroBlockedAdvertiser: ``, + MacroBlockedApp: ``, + MacroFD: ``, + MacroTransactionID: ``, + MacroPaymentIDChain: ``, + MacroCoppa: ``, + MacroDisplayManager: ``, + MacroDisplayManagerVersion: ``, + MacroInterstitial: ``, + MacroTagID: ``, + MacroBidFloor: ``, + MacroBidFloorCurrency: ``, + MacroSecure: ``, + MacroPMP: ``, + MacroVideoMIMES: ``, + MacroVideoMinimumDuration: ``, + MacroVideoMaximumDuration: ``, + MacroVideoProtocols: ``, + MacroVideoPlayerWidth: ``, + MacroVideoPlayerHeight: ``, + MacroVideoStartDelay: ``, + MacroVideoPlacement: ``, + MacroVideoLinearity: ``, + MacroVideoSkip: ``, + MacroVideoSkipMinimum: ``, + MacroVideoSkipAfter: ``, + MacroVideoSequence: ``, + MacroVideoBlockedAttribute: ``, + MacroVideoMaximumExtended: ``, + MacroVideoMinimumBitRate: ``, + MacroVideoMaximumBitRate: ``, + MacroVideoBoxing: ``, + MacroVideoPlaybackMethod: ``, + MacroVideoDelivery: ``, + MacroVideoPosition: ``, + MacroVideoAPI: ``, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: ``, + MacroDomain: ``, + MacroSectionCategory: ``, + MacroPageCategory: ``, + MacroPrivacyPolicy: ``, + MacroKeywords: ``, + MacroPubID: ``, + MacroPubName: ``, + MacroPubDomain: ``, + MacroContentID: ``, + MacroContentEpisode: ``, + MacroContentTitle: ``, + MacroContentSeries: ``, + MacroContentSeason: ``, + MacroContentArtist: ``, + MacroContentGenre: ``, + MacroContentAlbum: ``, + MacroContentISrc: ``, + MacroContentURL: ``, + MacroContentCategory: ``, + MacroContentProductionQuality: ``, + MacroContentVideoQuality: ``, + MacroContentContext: ``, + MacroContentContentRating: ``, + MacroContentUserRating: ``, + MacroContentQAGMediaRating: ``, + MacroContentKeywords: ``, + MacroContentLiveStream: ``, + MacroContentSourceRelationship: ``, + MacroContentLength: ``, + MacroContentLanguage: ``, + MacroContentEmbeddable: ``, + MacroProducerID: ``, + MacroProducerName: ``, + MacroUserAgent: ``, + MacroDNT: ``, + MacroLMT: ``, + MacroIP: ``, + MacroDeviceType: ``, + MacroMake: ``, + MacroModel: ``, + MacroDeviceOS: ``, + MacroDeviceOSVersion: ``, + MacroDeviceWidth: ``, + MacroDeviceHeight: ``, + MacroDeviceJS: ``, + MacroDeviceLanguage: ``, + MacroDeviceIFA: ``, + MacroDeviceDIDSHA1: ``, + MacroDeviceDIDMD5: ``, + MacroDeviceDPIDSHA1: ``, + MacroDeviceDPIDMD5: ``, + MacroDeviceMACSHA1: ``, + MacroDeviceMACMD5: ``, + MacroLatitude: ``, + MacroLongitude: ``, + MacroCountry: ``, + MacroRegion: ``, + MacroCity: ``, + MacroZip: ``, + MacroUTCOffset: ``, + MacroUserID: ``, + MacroYearOfBirth: ``, + MacroGender: ``, + MacroGDPRConsent: ``, + MacroGDPR: ``, + MacroUSPrivacy: ``, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `Site:RequestLevelMacros`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Test: 1, + TMax: 1000, + WSeat: []string{`wseat-1`, `wseat-2`}, + WLang: []string{`wlang-1`, `wlang-2`}, + BSeat: []string{`bseat-1`, `bseat-2`}, + Cur: []string{`usd`, `inr`}, + BCat: []string{`bcat-1`, `bcat-2`}, + BAdv: []string{`badv-1`, `badv-2`}, + BApp: []string{`bapp-1`, `bapp-2`}, + Source: &openrtb2.Source{ + FD: 1, + TID: `source-tid`, + PChain: `source-pchain`, + }, + Regs: &openrtb2.Regs{ + COPPA: 1, + Ext: []byte(`{"gdpr":1,"us_privacy":"user-privacy"}`), + }, + Imp: []openrtb2.Imp{ + { + DisplayManager: `disp-mgr`, + DisplayManagerVer: `1.2`, + Instl: 1, + TagID: `tag-id`, + BidFloor: 3.0, + BidFloorCur: `usd`, + Secure: new(int8), + PMP: &openrtb2.PMP{ + PrivateAuction: 1, + Deals: []openrtb2.Deal{ + { + ID: `deal-1`, + BidFloor: 4.0, + BidFloorCur: `usd`, + AT: 1, + WSeat: []string{`wseat-11`, `wseat-12`}, + WADomain: []string{`wdomain-11`, `wdomain-12`}, + }, + { + ID: `deal-2`, + BidFloor: 5.0, + BidFloorCur: `inr`, + AT: 1, + WSeat: []string{`wseat-21`, `wseat-22`}, + WADomain: []string{`wdomain-21`, `wdomain-22`}, + }, + }, + }, + Video: &openrtb2.Video{ + MIMEs: []string{`mp4`, `flv`}, + MinDuration: 30, + MaxDuration: 60, + Protocols: []openrtb2.Protocol{openrtb2.ProtocolVAST30, openrtb2.ProtocolVAST40Wrapper}, + Protocol: openrtb2.ProtocolVAST40Wrapper, + W: 640, + H: 480, + StartDelay: new(openrtb2.StartDelay), + Placement: openrtb2.VideoPlacementTypeInStream, + Linearity: openrtb2.VideoLinearityLinearInStream, + Skip: new(int8), + SkipMin: 10, + SkipAfter: 5, + Sequence: 1, + BAttr: []openrtb2.CreativeAttribute{openrtb2.CreativeAttributeAudioAdAutoPlay, openrtb2.CreativeAttributeAudioAdUserInitiated}, + MaxExtended: 10, + MinBitRate: 360, + MaxBitRate: 1080, + BoxingAllowed: 1, + PlaybackMethod: []openrtb2.PlaybackMethod{openrtb2.PlaybackMethodPageLoadSoundOn, openrtb2.PlaybackMethodClickSoundOn}, + PlaybackEnd: openrtb2.PlaybackCessationModeVideoCompletionOrTerminatedByUser, + Delivery: []openrtb2.ContentDeliveryMethod{openrtb2.ContentDeliveryMethodStreaming, openrtb2.ContentDeliveryMethodDownload}, + Pos: new(openrtb2.AdPosition), + API: []openrtb2.APIFramework{openrtb2.APIFrameworkVPAID10, openrtb2.APIFrameworkVPAID20}, + }, + }, + }, + Site: &openrtb2.Site{ + ID: `site-id`, + Name: `site-name`, + Domain: `site-domain`, + Cat: []string{`site-cat1`, `site-cat2`}, + SectionCat: []string{`site-sec-cat1`, `site-sec-cat2`}, + PageCat: []string{`site-page-cat1`, `site-page-cat2`}, + Page: `site-page-url`, + Ref: `site-referer-url`, + Search: `site-search-keywords`, + Mobile: 1, + PrivacyPolicy: 2, + Keywords: `site-keywords`, + Publisher: &openrtb2.Publisher{ + ID: `site-pub-id`, + Name: `site-pub-name`, + Domain: `site-pub-domain`, + }, + Content: &openrtb2.Content{ + ID: `site-cnt-id`, + Episode: 2, + Title: `site-cnt-title`, + Series: `site-cnt-series`, + Season: `site-cnt-season`, + Artist: `site-cnt-artist`, + Genre: `site-cnt-genre`, + Album: `site-cnt-album`, + ISRC: `site-cnt-isrc`, + URL: `site-cnt-url`, + Cat: []string{`site-cnt-cat1`, `site-cnt-cat2`}, + ProdQ: new(openrtb2.ProductionQuality), + VideoQuality: new(openrtb2.ProductionQuality), + Context: openrtb2.ContentContextVideo, + ContentRating: `1.2`, + UserRating: `2.2`, + QAGMediaRating: openrtb2.IQGMediaRatingAll, + Keywords: `site-cnt-keywords`, + LiveStream: 1, + SourceRelationship: 1, + Len: 100, + Language: `english`, + Embeddable: 1, + Producer: &openrtb2.Producer{ + ID: `site-cnt-prod-id`, + Name: `site-cnt-prod-name`, + }, + }, + }, + Device: &openrtb2.Device{ + UA: `user-agent`, + DNT: new(int8), + Lmt: new(int8), + IP: `ipv4`, + IPv6: `ipv6`, + DeviceType: openrtb2.DeviceTypeConnectedTV, + Make: `device-make`, + Model: `device-model`, + OS: `os`, + OSV: `os-version`, + H: 1024, + W: 2048, + JS: 1, + Language: `device-lang`, + ConnectionType: new(openrtb2.ConnectionType), + IFA: `ifa`, + DIDSHA1: `didsha1`, + DIDMD5: `didmd5`, + DPIDSHA1: `dpidsha1`, + DPIDMD5: `dpidmd5`, + MACSHA1: `macsha1`, + MACMD5: `macmd5`, + Geo: &openrtb2.Geo{ + Lat: 1.1, + Lon: 2.2, + Country: `country`, + Region: `region`, + City: `city`, + ZIP: `zip`, + UTCOffset: 1000, + }, + }, + User: &openrtb2.User{ + ID: `user-id`, + Yob: 1990, + Gender: `M`, + Ext: []byte(`{"consent":"user-gdpr-consent"}`), + }, + }, + }, + macros: map[string]string{ + MacroTest: `1`, + MacroTimeout: `1000`, + MacroWhitelistSeat: `wseat-1,wseat-2`, + MacroWhitelistLang: `wlang-1,wlang-2`, + MacroBlockedSeat: `bseat-1,bseat-2`, + MacroCurrency: `usd,inr`, + MacroBlockedCategory: `bcat-1,bcat-2`, + MacroBlockedAdvertiser: `badv-1,badv-2`, + MacroBlockedApp: `bapp-1,bapp-2`, + MacroFD: `1`, + MacroTransactionID: `source-tid`, + MacroPaymentIDChain: `source-pchain`, + MacroCoppa: `1`, + MacroDisplayManager: `disp-mgr`, + MacroDisplayManagerVersion: `1.2`, + MacroInterstitial: `1`, + MacroTagID: `tag-id`, + MacroBidFloor: `3`, + MacroBidFloorCurrency: `usd`, + MacroSecure: `0`, + MacroPMP: `{"private_auction":1,"deals":[{"id":"deal-1","bidfloor":4,"bidfloorcur":"usd","at":1,"wseat":["wseat-11","wseat-12"],"wadomain":["wdomain-11","wdomain-12"]},{"id":"deal-2","bidfloor":5,"bidfloorcur":"inr","at":1,"wseat":["wseat-21","wseat-22"],"wadomain":["wdomain-21","wdomain-22"]}]}`, + MacroVideoMIMES: `mp4,flv`, + MacroVideoMinimumDuration: `30`, + MacroVideoMaximumDuration: `60`, + MacroVideoProtocols: `3,8`, + MacroVideoPlayerWidth: `640`, + MacroVideoPlayerHeight: `480`, + MacroVideoStartDelay: `0`, + MacroVideoPlacement: `1`, + MacroVideoLinearity: `1`, + MacroVideoSkip: `0`, + MacroVideoSkipMinimum: `10`, + MacroVideoSkipAfter: `5`, + MacroVideoSequence: `1`, + MacroVideoBlockedAttribute: `1,2`, + MacroVideoMaximumExtended: `10`, + MacroVideoMinimumBitRate: `360`, + MacroVideoMaximumBitRate: `1080`, + MacroVideoBoxing: `1`, + MacroVideoPlaybackMethod: `1,3`, + MacroVideoDelivery: `1,3`, + MacroVideoPosition: `0`, + MacroVideoAPI: `1,2`, + MacroSiteID: `site-id`, + MacroSiteName: `site-name`, + MacroSitePage: `site-page-url`, + MacroSiteReferrer: `site-referer-url`, + MacroSiteSearch: `site-search-keywords`, + MacroSiteMobile: `1`, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: `site-cat1,site-cat2`, + MacroDomain: `site-domain`, + MacroSectionCategory: `site-sec-cat1,site-sec-cat2`, + MacroPageCategory: `site-page-cat1,site-page-cat2`, + MacroPrivacyPolicy: `2`, + MacroKeywords: `site-keywords`, + MacroPubID: `site-pub-id`, + MacroPubName: `site-pub-name`, + MacroPubDomain: `site-pub-domain`, + MacroContentID: `site-cnt-id`, + MacroContentEpisode: `2`, + MacroContentTitle: `site-cnt-title`, + MacroContentSeries: `site-cnt-series`, + MacroContentSeason: `site-cnt-season`, + MacroContentArtist: `site-cnt-artist`, + MacroContentGenre: `site-cnt-genre`, + MacroContentAlbum: `site-cnt-album`, + MacroContentISrc: `site-cnt-isrc`, + MacroContentURL: `site-cnt-url`, + MacroContentCategory: `site-cnt-cat1,site-cnt-cat2`, + MacroContentProductionQuality: `0`, + MacroContentVideoQuality: `0`, + MacroContentContext: `1`, + MacroContentContentRating: `1.2`, + MacroContentUserRating: `2.2`, + MacroContentQAGMediaRating: `1`, + MacroContentKeywords: `site-cnt-keywords`, + MacroContentLiveStream: `1`, + MacroContentSourceRelationship: `1`, + MacroContentLength: `100`, + MacroContentLanguage: `english`, + MacroContentEmbeddable: `1`, + MacroProducerID: `site-cnt-prod-id`, + MacroProducerName: `site-cnt-prod-name`, + MacroUserAgent: `user-agent`, + MacroDNT: `0`, + MacroLMT: `0`, + MacroIP: `ipv4`, + MacroDeviceType: `3`, + MacroMake: `device-make`, + MacroModel: `device-model`, + MacroDeviceOS: `os`, + MacroDeviceOSVersion: `os-version`, + MacroDeviceWidth: `2048`, + MacroDeviceHeight: `1024`, + MacroDeviceJS: `1`, + MacroDeviceLanguage: `device-lang`, + MacroDeviceIFA: `ifa`, + MacroDeviceDIDSHA1: `didsha1`, + MacroDeviceDIDMD5: `didmd5`, + MacroDeviceDPIDSHA1: `dpidsha1`, + MacroDeviceDPIDMD5: `dpidmd5`, + MacroDeviceMACSHA1: `macsha1`, + MacroDeviceMACMD5: `macmd5`, + MacroLatitude: `1.1`, + MacroLongitude: `2.2`, + MacroCountry: `country`, + MacroRegion: `region`, + MacroCity: `city`, + MacroZip: `zip`, + MacroUTCOffset: `1000`, + MacroUserID: `user-id`, + MacroYearOfBirth: `1990`, + MacroGender: `M`, + MacroGDPRConsent: `user-gdpr-consent`, + MacroGDPR: `1`, + MacroUSPrivacy: `user-privacy`, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `App:RequestLevelMacros`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Test: 1, + TMax: 1000, + WSeat: []string{`wseat-1`, `wseat-2`}, + WLang: []string{`wlang-1`, `wlang-2`}, + BSeat: []string{`bseat-1`, `bseat-2`}, + Cur: []string{`usd`, `inr`}, + BCat: []string{`bcat-1`, `bcat-2`}, + BAdv: []string{`badv-1`, `badv-2`}, + BApp: []string{`bapp-1`, `bapp-2`}, + Source: &openrtb2.Source{ + FD: 1, + TID: `source-tid`, + PChain: `source-pchain`, + }, + Regs: &openrtb2.Regs{ + COPPA: 1, + Ext: []byte(`{"gdpr":1,"us_privacy":"user-privacy"}`), + }, + Imp: []openrtb2.Imp{ + { + DisplayManager: `disp-mgr`, + DisplayManagerVer: `1.2`, + Instl: 1, + TagID: `tag-id`, + BidFloor: 3.0, + BidFloorCur: `usd`, + Secure: new(int8), + PMP: &openrtb2.PMP{ + PrivateAuction: 1, + Deals: []openrtb2.Deal{ + { + ID: `deal-1`, + BidFloor: 4.0, + BidFloorCur: `usd`, + AT: 1, + WSeat: []string{`wseat-11`, `wseat-12`}, + WADomain: []string{`wdomain-11`, `wdomain-12`}, + }, + { + ID: `deal-2`, + BidFloor: 5.0, + BidFloorCur: `inr`, + AT: 1, + WSeat: []string{`wseat-21`, `wseat-22`}, + WADomain: []string{`wdomain-21`, `wdomain-22`}, + }, + }, + }, + Video: &openrtb2.Video{ + MIMEs: []string{`mp4`, `flv`}, + MinDuration: 30, + MaxDuration: 60, + Protocols: []openrtb2.Protocol{openrtb2.ProtocolVAST30, openrtb2.ProtocolVAST40Wrapper}, + Protocol: openrtb2.ProtocolVAST40Wrapper, + W: 640, + H: 480, + StartDelay: new(openrtb2.StartDelay), + Placement: openrtb2.VideoPlacementTypeInStream, + Linearity: openrtb2.VideoLinearityLinearInStream, + Skip: new(int8), + SkipMin: 10, + SkipAfter: 5, + Sequence: 1, + BAttr: []openrtb2.CreativeAttribute{openrtb2.CreativeAttributeAudioAdAutoPlay, openrtb2.CreativeAttributeAudioAdUserInitiated}, + MaxExtended: 10, + MinBitRate: 360, + MaxBitRate: 1080, + BoxingAllowed: 1, + PlaybackMethod: []openrtb2.PlaybackMethod{openrtb2.PlaybackMethodPageLoadSoundOn, openrtb2.PlaybackMethodClickSoundOn}, + PlaybackEnd: openrtb2.PlaybackCessationModeVideoCompletionOrTerminatedByUser, + Delivery: []openrtb2.ContentDeliveryMethod{openrtb2.ContentDeliveryMethodStreaming, openrtb2.ContentDeliveryMethodDownload}, + Pos: new(openrtb2.AdPosition), + API: []openrtb2.APIFramework{openrtb2.APIFrameworkVPAID10, openrtb2.APIFrameworkVPAID20}, + }, + }, + }, + App: &openrtb2.App{ + ID: `app-id`, + Bundle: `app-bundle`, + StoreURL: `app-store-url`, + Ver: `app-version`, + Paid: 1, + Name: `app-name`, + Domain: `app-domain`, + Cat: []string{`app-cat1`, `app-cat2`}, + SectionCat: []string{`app-sec-cat1`, `app-sec-cat2`}, + PageCat: []string{`app-page-cat1`, `app-page-cat2`}, + PrivacyPolicy: 2, + Keywords: `app-keywords`, + Publisher: &openrtb2.Publisher{ + ID: `app-pub-id`, + Name: `app-pub-name`, + Domain: `app-pub-domain`, + }, + Content: &openrtb2.Content{ + ID: `app-cnt-id`, + Episode: 2, + Title: `app-cnt-title`, + Series: `app-cnt-series`, + Season: `app-cnt-season`, + Artist: `app-cnt-artist`, + Genre: `app-cnt-genre`, + Album: `app-cnt-album`, + ISRC: `app-cnt-isrc`, + URL: `app-cnt-url`, + Cat: []string{`app-cnt-cat1`, `app-cnt-cat2`}, + ProdQ: new(openrtb2.ProductionQuality), + VideoQuality: new(openrtb2.ProductionQuality), + Context: openrtb2.ContentContextVideo, + ContentRating: `1.2`, + UserRating: `2.2`, + QAGMediaRating: openrtb2.IQGMediaRatingAll, + Keywords: `app-cnt-keywords`, + LiveStream: 1, + SourceRelationship: 1, + Len: 100, + Language: `english`, + Embeddable: 1, + Producer: &openrtb2.Producer{ + ID: `app-cnt-prod-id`, + Name: `app-cnt-prod-name`, + }, + }, + }, + Device: &openrtb2.Device{ + UA: `user-agent`, + DNT: new(int8), + Lmt: new(int8), + IPv6: `ipv6`, + DeviceType: openrtb2.DeviceTypeConnectedTV, + Make: `device-make`, + Model: `device-model`, + OS: `os`, + OSV: `os-version`, + H: 1024, + W: 2048, + JS: 1, + Language: `device-lang`, + ConnectionType: new(openrtb2.ConnectionType), + IFA: `ifa`, + DIDSHA1: `didsha1`, + DIDMD5: `didmd5`, + DPIDSHA1: `dpidsha1`, + DPIDMD5: `dpidmd5`, + MACSHA1: `macsha1`, + MACMD5: `macmd5`, + Geo: &openrtb2.Geo{ + Lat: 1.1, + Lon: 2.2, + Country: `country`, + Region: `region`, + City: `city`, + ZIP: `zip`, + UTCOffset: 1000, + }, + }, + User: &openrtb2.User{ + ID: `user-id`, + Yob: 1990, + Gender: `M`, + Ext: []byte(`{"consent":"user-gdpr-consent"}`), + }, + }, + }, + macros: map[string]string{ + MacroTest: `1`, + MacroTimeout: `1000`, + MacroWhitelistSeat: `wseat-1,wseat-2`, + MacroWhitelistLang: `wlang-1,wlang-2`, + MacroBlockedSeat: `bseat-1,bseat-2`, + MacroCurrency: `usd,inr`, + MacroBlockedCategory: `bcat-1,bcat-2`, + MacroBlockedAdvertiser: `badv-1,badv-2`, + MacroBlockedApp: `bapp-1,bapp-2`, + MacroFD: `1`, + MacroTransactionID: `source-tid`, + MacroPaymentIDChain: `source-pchain`, + MacroCoppa: `1`, + MacroDisplayManager: `disp-mgr`, + MacroDisplayManagerVersion: `1.2`, + MacroInterstitial: `1`, + MacroTagID: `tag-id`, + MacroBidFloor: `3`, + MacroBidFloorCurrency: `usd`, + MacroSecure: `0`, + MacroPMP: `{"private_auction":1,"deals":[{"id":"deal-1","bidfloor":4,"bidfloorcur":"usd","at":1,"wseat":["wseat-11","wseat-12"],"wadomain":["wdomain-11","wdomain-12"]},{"id":"deal-2","bidfloor":5,"bidfloorcur":"inr","at":1,"wseat":["wseat-21","wseat-22"],"wadomain":["wdomain-21","wdomain-22"]}]}`, + MacroVideoMIMES: `mp4,flv`, + MacroVideoMinimumDuration: `30`, + MacroVideoMaximumDuration: `60`, + MacroVideoProtocols: `3,8`, + MacroVideoPlayerWidth: `640`, + MacroVideoPlayerHeight: `480`, + MacroVideoStartDelay: `0`, + MacroVideoPlacement: `1`, + MacroVideoLinearity: `1`, + MacroVideoSkip: `0`, + MacroVideoSkipMinimum: `10`, + MacroVideoSkipAfter: `5`, + MacroVideoSequence: `1`, + MacroVideoBlockedAttribute: `1,2`, + MacroVideoMaximumExtended: `10`, + MacroVideoMinimumBitRate: `360`, + MacroVideoMaximumBitRate: `1080`, + MacroVideoBoxing: `1`, + MacroVideoPlaybackMethod: `1,3`, + MacroVideoDelivery: `1,3`, + MacroVideoPosition: `0`, + MacroVideoAPI: `1,2`, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: `app-id`, + MacroAppName: `app-name`, + MacroAppBundle: `app-bundle`, + MacroAppStoreURL: `app-store-url`, + MacroAppVersion: `app-version`, + MacroAppPaid: `1`, + MacroCategory: `app-cat1,app-cat2`, + MacroDomain: `app-domain`, + MacroSectionCategory: `app-sec-cat1,app-sec-cat2`, + MacroPageCategory: `app-page-cat1,app-page-cat2`, + MacroPrivacyPolicy: `2`, + MacroKeywords: `app-keywords`, + MacroPubID: `app-pub-id`, + MacroPubName: `app-pub-name`, + MacroPubDomain: `app-pub-domain`, + MacroContentID: `app-cnt-id`, + MacroContentEpisode: `2`, + MacroContentTitle: `app-cnt-title`, + MacroContentSeries: `app-cnt-series`, + MacroContentSeason: `app-cnt-season`, + MacroContentArtist: `app-cnt-artist`, + MacroContentGenre: `app-cnt-genre`, + MacroContentAlbum: `app-cnt-album`, + MacroContentISrc: `app-cnt-isrc`, + MacroContentURL: `app-cnt-url`, + MacroContentCategory: `app-cnt-cat1,app-cnt-cat2`, + MacroContentProductionQuality: `0`, + MacroContentVideoQuality: `0`, + MacroContentContext: `1`, + MacroContentContentRating: `1.2`, + MacroContentUserRating: `2.2`, + MacroContentQAGMediaRating: `1`, + MacroContentKeywords: `app-cnt-keywords`, + MacroContentLiveStream: `1`, + MacroContentSourceRelationship: `1`, + MacroContentLength: `100`, + MacroContentLanguage: `english`, + MacroContentEmbeddable: `1`, + MacroProducerID: `app-cnt-prod-id`, + MacroProducerName: `app-cnt-prod-name`, + MacroUserAgent: `user-agent`, + MacroDNT: `0`, + MacroLMT: `0`, + MacroIP: `ipv6`, + MacroDeviceType: `3`, + MacroMake: `device-make`, + MacroModel: `device-model`, + MacroDeviceOS: `os`, + MacroDeviceOSVersion: `os-version`, + MacroDeviceWidth: `2048`, + MacroDeviceHeight: `1024`, + MacroDeviceJS: `1`, + MacroDeviceLanguage: `device-lang`, + MacroDeviceIFA: `ifa`, + MacroDeviceDIDSHA1: `didsha1`, + MacroDeviceDIDMD5: `didmd5`, + MacroDeviceDPIDSHA1: `dpidsha1`, + MacroDeviceDPIDMD5: `dpidmd5`, + MacroDeviceMACSHA1: `macsha1`, + MacroDeviceMACMD5: `macmd5`, + MacroLatitude: `1.1`, + MacroLongitude: `2.2`, + MacroCountry: `country`, + MacroRegion: `region`, + MacroCity: `city`, + MacroZip: `zip`, + MacroUTCOffset: `1000`, + MacroUserID: `user-id`, + MacroYearOfBirth: `1990`, + MacroGender: `M`, + MacroGDPRConsent: `user-gdpr-consent`, + MacroGDPR: `1`, + MacroUSPrivacy: `user-privacy`, + MacroCacheBuster: `cachebuster`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + macroMappings := GetDefaultMapper() + + tag := tt.args.tag + tag.InitBidRequest(tt.args.bidRequest) + tag.SetAdapterConfig(tt.args.conf) + tag.LoadImpression(&tt.args.bidRequest.Imp[0]) + + for key, result := range tt.macros { + cb, ok := macroMappings[key] + if !ok { + assert.NotEmpty(t, result) + } else { + actual := cb.callback(tag, key) + assert.Equal(t, result, actual, fmt.Sprintf("MacroFunction: %v", key)) + } + } + }) + } +} diff --git a/adapters/vastbidder/constant.go b/adapters/vastbidder/constant.go new file mode 100644 index 00000000000..08b524d2bca --- /dev/null +++ b/adapters/vastbidder/constant.go @@ -0,0 +1,166 @@ +package vastbidder + +const ( + intBase = 10 + comma = `,` +) + +//List of Tag Bidder Macros +const ( + //Request + MacroTest = `test` + MacroTimeout = `timeout` + MacroWhitelistSeat = `wseat` + MacroWhitelistLang = `wlang` + MacroBlockedSeat = `bseat` + MacroCurrency = `cur` + MacroBlockedCategory = `bcat` + MacroBlockedAdvertiser = `badv` + MacroBlockedApp = `bapp` + + //Source + MacroFD = `fd` + MacroTransactionID = `tid` + MacroPaymentIDChain = `pchain` + + //Regs + MacroCoppa = `coppa` + + //Impression + MacroDisplayManager = `displaymanager` + MacroDisplayManagerVersion = `displaymanagerver` + MacroInterstitial = `instl` + MacroTagID = `tagid` + MacroBidFloor = `bidfloor` + MacroBidFloorCurrency = `bidfloorcur` + MacroSecure = `secure` + MacroPMP = `pmp` + + //Video + MacroVideoMIMES = `mimes` + MacroVideoMinimumDuration = `minduration` + MacroVideoMaximumDuration = `maxduration` + MacroVideoProtocols = `protocols` + MacroVideoPlayerWidth = `playerwidth` + MacroVideoPlayerHeight = `playerheight` + MacroVideoStartDelay = `startdelay` + MacroVideoPlacement = `placement` + MacroVideoLinearity = `linearity` + MacroVideoSkip = `skip` + MacroVideoSkipMinimum = `skipmin` + MacroVideoSkipAfter = `skipafter` + MacroVideoSequence = `sequence` + MacroVideoBlockedAttribute = `battr` + MacroVideoMaximumExtended = `maxextended` + MacroVideoMinimumBitRate = `minbitrate` + MacroVideoMaximumBitRate = `maxbitrate` + MacroVideoBoxing = `boxingallowed` + MacroVideoPlaybackMethod = `playbackmethod` + MacroVideoDelivery = `delivery` + MacroVideoPosition = `position` + MacroVideoAPI = `api` + + //Site + MacroSiteID = `siteid` + MacroSiteName = `sitename` + MacroSitePage = `page` + MacroSiteReferrer = `ref` + MacroSiteSearch = `search` + MacroSiteMobile = `mobile` + + //App + MacroAppID = `appid` + MacroAppName = `appname` + MacroAppBundle = `bundle` + MacroAppStoreURL = `storeurl` + MacroAppVersion = `appver` + MacroAppPaid = `paid` + + //SiteAppCommon + MacroCategory = `cat` + MacroDomain = `domain` + MacroSectionCategory = `sectioncat` + MacroPageCategory = `pagecat` + MacroPrivacyPolicy = `privacypolicy` + MacroKeywords = `keywords` + + //Publisher + MacroPubID = `pubid` + MacroPubName = `pubname` + MacroPubDomain = `pubdomain` + + //Content + MacroContentID = `contentid` + MacroContentEpisode = `episode` + MacroContentTitle = `title` + MacroContentSeries = `series` + MacroContentSeason = `season` + MacroContentArtist = `artist` + MacroContentGenre = `genre` + MacroContentAlbum = `album` + MacroContentISrc = `isrc` + MacroContentURL = `contenturl` + MacroContentCategory = `contentcat` + MacroContentProductionQuality = `contentprodq` + MacroContentVideoQuality = `contentvideoquality` + MacroContentContext = `context` + MacroContentContentRating = `contentrating` + MacroContentUserRating = `userrating` + MacroContentQAGMediaRating = `qagmediarating` + MacroContentKeywords = `contentkeywords` + MacroContentLiveStream = `livestream` + MacroContentSourceRelationship = `sourcerelationship` + MacroContentLength = `contentlen` + MacroContentLanguage = `contentlanguage` + MacroContentEmbeddable = `contentembeddable` + + //Producer + MacroProducerID = `prodid` + MacroProducerName = `prodname` + + //Device + MacroUserAgent = `useragent` + MacroDNT = `dnt` + MacroLMT = `lmt` + MacroIP = `ip` + MacroDeviceType = `devicetype` + MacroMake = `make` + MacroModel = `model` + MacroDeviceOS = `os` + MacroDeviceOSVersion = `osv` + MacroDeviceWidth = `devicewidth` + MacroDeviceHeight = `deviceheight` + MacroDeviceJS = `js` + MacroDeviceLanguage = `lang` + MacroDeviceIFA = `ifa` + MacroDeviceDIDSHA1 = `didsha1` + MacroDeviceDIDMD5 = `didmd5` + MacroDeviceDPIDSHA1 = `dpidsha1` + MacroDeviceDPIDMD5 = `dpidmd5` + MacroDeviceMACSHA1 = `macsha1` + MacroDeviceMACMD5 = `macmd5` + + //Geo + MacroLatitude = `lat` + MacroLongitude = `lon` + MacroCountry = `country` + MacroRegion = `region` + MacroCity = `city` + MacroZip = `zip` + MacroUTCOffset = `utcoffset` + + //User + MacroUserID = `uid` + MacroYearOfBirth = `yob` + MacroGender = `gender` + + //Extension + MacroGDPRConsent = `consent` + MacroGDPR = `gdpr` + MacroUSPrivacy = `usprivacy` + + //Additional + MacroCacheBuster = `cachebuster` +) + +var ParamKeys = []string{"param1", "param2", "param3", "param4", "param5"} diff --git a/adapters/vastbidder/ibidder_macro.go b/adapters/vastbidder/ibidder_macro.go new file mode 100644 index 00000000000..d5531b70413 --- /dev/null +++ b/adapters/vastbidder/ibidder_macro.go @@ -0,0 +1,199 @@ +package vastbidder + +import ( + "net/http" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//Flags of each tag bidder +type Flags struct { + RemoveEmptyParam bool `json:"remove_empty,omitempty"` +} + +//IBidderMacro interface will capture all macro definition +type IBidderMacro interface { + //Helper Function + InitBidRequest(request *openrtb2.BidRequest) + LoadImpression(imp *openrtb2.Imp) (*openrtb_ext.ExtImpVASTBidder, error) + LoadVASTTag(tag *openrtb_ext.ExtImpVASTBidderTag) + GetBidderKeys() map[string]string + SetAdapterConfig(*config.Adapter) + GetURI() string + GetHeaders() http.Header + //getAllHeaders returns default and custom heades + getAllHeaders() http.Header + + //Request + MacroTest(string) string + MacroTimeout(string) string + MacroWhitelistSeat(string) string + MacroWhitelistLang(string) string + MacroBlockedSeat(string) string + MacroCurrency(string) string + MacroBlockedCategory(string) string + MacroBlockedAdvertiser(string) string + MacroBlockedApp(string) string + + //Source + MacroFD(string) string + MacroTransactionID(string) string + MacroPaymentIDChain(string) string + + //Regs + MacroCoppa(string) string + + //Impression + MacroDisplayManager(string) string + MacroDisplayManagerVersion(string) string + MacroInterstitial(string) string + MacroTagID(string) string + MacroBidFloor(string) string + MacroBidFloorCurrency(string) string + MacroSecure(string) string + MacroPMP(string) string + + //Video + MacroVideoMIMES(string) string + MacroVideoMinimumDuration(string) string + MacroVideoMaximumDuration(string) string + MacroVideoProtocols(string) string + MacroVideoPlayerWidth(string) string + MacroVideoPlayerHeight(string) string + MacroVideoStartDelay(string) string + MacroVideoPlacement(string) string + MacroVideoLinearity(string) string + MacroVideoSkip(string) string + MacroVideoSkipMinimum(string) string + MacroVideoSkipAfter(string) string + MacroVideoSequence(string) string + MacroVideoBlockedAttribute(string) string + MacroVideoMaximumExtended(string) string + MacroVideoMinimumBitRate(string) string + MacroVideoMaximumBitRate(string) string + MacroVideoBoxing(string) string + MacroVideoPlaybackMethod(string) string + MacroVideoDelivery(string) string + MacroVideoPosition(string) string + MacroVideoAPI(string) string + + //Site + MacroSiteID(string) string + MacroSiteName(string) string + MacroSitePage(string) string + MacroSiteReferrer(string) string + MacroSiteSearch(string) string + MacroSiteMobile(string) string + + //App + MacroAppID(string) string + MacroAppName(string) string + MacroAppBundle(string) string + MacroAppStoreURL(string) string + MacroAppVersion(string) string + MacroAppPaid(string) string + + //SiteAppCommon + MacroCategory(string) string + MacroDomain(string) string + MacroSectionCategory(string) string + MacroPageCategory(string) string + MacroPrivacyPolicy(string) string + MacroKeywords(string) string + + //Publisher + MacroPubID(string) string + MacroPubName(string) string + MacroPubDomain(string) string + + //Content + MacroContentID(string) string + MacroContentEpisode(string) string + MacroContentTitle(string) string + MacroContentSeries(string) string + MacroContentSeason(string) string + MacroContentArtist(string) string + MacroContentGenre(string) string + MacroContentAlbum(string) string + MacroContentISrc(string) string + MacroContentURL(string) string + MacroContentCategory(string) string + MacroContentProductionQuality(string) string + MacroContentVideoQuality(string) string + MacroContentContext(string) string + MacroContentContentRating(string) string + MacroContentUserRating(string) string + MacroContentQAGMediaRating(string) string + MacroContentKeywords(string) string + MacroContentLiveStream(string) string + MacroContentSourceRelationship(string) string + MacroContentLength(string) string + MacroContentLanguage(string) string + MacroContentEmbeddable(string) string + + //Producer + MacroProducerID(string) string + MacroProducerName(string) string + + //Device + MacroUserAgent(string) string + MacroDNT(string) string + MacroLMT(string) string + MacroIP(string) string + MacroDeviceType(string) string + MacroMake(string) string + MacroModel(string) string + MacroDeviceOS(string) string + MacroDeviceOSVersion(string) string + MacroDeviceWidth(string) string + MacroDeviceHeight(string) string + MacroDeviceJS(string) string + MacroDeviceLanguage(string) string + MacroDeviceIFA(string) string + MacroDeviceDIDSHA1(string) string + MacroDeviceDIDMD5(string) string + MacroDeviceDPIDSHA1(string) string + MacroDeviceDPIDMD5(string) string + MacroDeviceMACSHA1(string) string + MacroDeviceMACMD5(string) string + + //Geo + MacroLatitude(string) string + MacroLongitude(string) string + MacroCountry(string) string + MacroRegion(string) string + MacroCity(string) string + MacroZip(string) string + MacroUTCOffset(string) string + + //User + MacroUserID(string) string + MacroYearOfBirth(string) string + MacroGender(string) string + + //Extension + MacroGDPRConsent(string) string + MacroGDPR(string) string + MacroUSPrivacy(string) string + + //Additional + MacroCacheBuster(string) string +} + +var bidderMacroMap = map[openrtb_ext.BidderName]func() IBidderMacro{} + +//RegisterNewBidderMacro will be used by each bidder to set its respective macro IBidderMacro +func RegisterNewBidderMacro(bidder openrtb_ext.BidderName, macro func() IBidderMacro) { + bidderMacroMap[bidder] = macro +} + +//GetNewBidderMacro will return IBidderMacro of specific bidder +func GetNewBidderMacro(bidder openrtb_ext.BidderName) IBidderMacro { + callback, ok := bidderMacroMap[bidder] + if ok { + return callback() + } + return NewBidderMacro() +} diff --git a/adapters/vastbidder/itag_response_handler.go b/adapters/vastbidder/itag_response_handler.go new file mode 100644 index 00000000000..fd6d8b3357a --- /dev/null +++ b/adapters/vastbidder/itag_response_handler.go @@ -0,0 +1,43 @@ +package vastbidder + +import ( + "errors" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" +) + +//ITagRequestHandler parse bidder request +type ITagRequestHandler interface { + MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) +} + +//ITagResponseHandler parse bidder response +type ITagResponseHandler interface { + Validate(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) []error + MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) +} + +//HandlerType list of tag based response handlers +type HandlerType string + +const ( + VASTTagHandlerType HandlerType = `vasttag` +) + +//GetResponseHandler returns response handler +func GetResponseHandler(responseType HandlerType) (ITagResponseHandler, error) { + switch responseType { + case VASTTagHandlerType: + return NewVASTTagResponseHandler(), nil + } + return nil, errors.New(`Unkown Response Handler`) +} + +func GetRequestHandler(responseType HandlerType) (ITagRequestHandler, error) { + switch responseType { + case VASTTagHandlerType: + return nil, nil + } + return nil, errors.New(`Unkown Response Handler`) +} diff --git a/adapters/vastbidder/macro_processor.go b/adapters/vastbidder/macro_processor.go new file mode 100644 index 00000000000..47e15d5178a --- /dev/null +++ b/adapters/vastbidder/macro_processor.go @@ -0,0 +1,216 @@ +package vastbidder + +import ( + "bytes" + "net/url" + "strings" + + "github.com/golang/glog" +) + +const ( + macroPrefix string = `{` //macro prefix can not be empty + macroSuffix string = `}` //macro suffix can not be empty + macroEscapeSuffix string = `_ESC` + macroPrefixLen int = len(macroPrefix) + macroSuffixLen int = len(macroSuffix) + macroEscapeSuffixLen int = len(macroEscapeSuffix) +) + +//Flags to customize macro processing wrappers + +//MacroProcessor struct to hold openrtb request and cache values +type MacroProcessor struct { + bidderMacro IBidderMacro + mapper Mapper + macroCache map[string]string + bidderKeys map[string]string +} + +//NewMacroProcessor will process macro's of openrtb bid request +func NewMacroProcessor(bidderMacro IBidderMacro, mapper Mapper) *MacroProcessor { + return &MacroProcessor{ + bidderMacro: bidderMacro, + mapper: mapper, + macroCache: make(map[string]string), + } +} + +//SetMacro Adding Custom Macro Manually +func (mp *MacroProcessor) SetMacro(key, value string) { + mp.macroCache[key] = value +} + +//SetBidderKeys will flush and set bidder specific keys +func (mp *MacroProcessor) SetBidderKeys(keys map[string]string) { + mp.bidderKeys = keys +} + +//processKey : returns value of key macro and status found or not +func (mp *MacroProcessor) processKey(key string) (string, bool) { + var valueCallback *macroCallBack + var value string + nEscaping := 0 + tmpKey := key + found := false + + for { + //Search in macro cache + if value, found = mp.macroCache[tmpKey]; found { + break + } + + //Search for bidder keys + if nil != mp.bidderKeys { + if value, found = mp.bidderKeys[tmpKey]; found { + break + } + } + + valueCallback, found = mp.mapper[tmpKey] + if found { + //found callback function + value = valueCallback.callback(mp.bidderMacro, tmpKey) + break + } else if strings.HasSuffix(tmpKey, macroEscapeSuffix) { + //escaping macro found + tmpKey = tmpKey[0 : len(tmpKey)-macroEscapeSuffixLen] + nEscaping++ + continue + } + break + } + + if found { + if len(value) > 0 { + if nEscaping > 0 { + //escaping string nEscaping times + value = escape(value, nEscaping) + } + if nil != valueCallback && valueCallback.cached { + //cached value if its cached flag is true + mp.macroCache[key] = value + } + } + } + + return value, found +} + +//ProcessString : Substitute macros in input string +func (mp *MacroProcessor) ProcessString(in string) (response string) { + var out bytes.Buffer + pos, start, end, size := 0, 0, 0, len(in) + + for pos < size { + //find macro prefix index + if start = strings.Index(in[pos:], macroPrefix); -1 == start { + //[prefix_not_found] append remaining string to response + out.WriteString(in[pos:]) + + //macro prefix not found + break + } + + //prefix index w.r.t original string + start = start + pos + + //append non macro prefix content + out.WriteString(in[pos:start]) + + if (end - macroSuffixLen) <= (start + macroPrefixLen) { + //string contains {{TEXT_{{MACRO}} -> it should replace it with{{TEXT_MACROVALUE + //find macro suffix index + if end = strings.Index(in[start+macroPrefixLen:], macroSuffix); -1 == end { + //[suffix_not_found] append remaining string to response + out.WriteString(in[start:]) + + // We Found First %% and Not Found Second %% But We are in between of string + break + } + + end = start + macroPrefixLen + end + macroSuffixLen + } + + //get actual macro key by removing macroPrefix and macroSuffix from key itself + key := in[start+macroPrefixLen : end-macroSuffixLen] + + //process macro + value, found := mp.processKey(key) + if found { + out.WriteString(value) + pos = end + } else { + out.WriteByte(macroPrefix[0]) + pos = start + 1 + } + //glog.Infof("\nSearch[%d] : [%d,%d,%s]", count, start, end, key) + } + response = out.String() + glog.V(3).Infof("[MACRO]:in:[%s] replaced:[%s]", in, response) + return +} + +//ProcessURL : Substitute macros in input string +func (mp *MacroProcessor) ProcessURL(uri string, flags Flags) (response string) { + if !flags.RemoveEmptyParam { + return mp.ProcessString(uri) + } + + murl, _ := url.Parse(uri) + + murl.Path = mp.ProcessString(murl.Path) + murl.RawQuery = mp.processURLValues(murl.Query(), flags) + murl.Fragment = mp.ProcessString(murl.Fragment) + + response = murl.String() + + glog.V(3).Infof("[MACRO]:in:[%s] replaced:[%s]", uri, response) + return +} + +//processURLValues : returns replaced macro values of url.values +func (mp *MacroProcessor) processURLValues(values url.Values, flags Flags) (response string) { + var out bytes.Buffer + for k, v := range values { + macroKey := v[0] + found := false + value := "" + + if len(macroKey) > (macroPrefixLen+macroSuffixLen) && + strings.HasPrefix(macroKey, macroPrefix) && + strings.HasSuffix(macroKey, macroSuffix) { + //Check macro key directly if present + newKey := macroKey[macroPrefixLen : len(macroKey)-macroSuffixLen] + value, found = mp.processKey(newKey) + } + + if !found { + //if key is not present then process it as normal string + value = mp.ProcessString(macroKey) + } + + if flags.RemoveEmptyParam == false || len(value) > 0 { + //append + if out.Len() > 0 { + out.WriteByte('&') + } + out.WriteString(k) + out.WriteByte('=') + out.WriteString(url.QueryEscape(value)) + } + } + return out.String() +} + +//GetMacroKey will return macro formatted key +func GetMacroKey(key string) string { + return macroPrefix + key + macroSuffix +} + +func escape(str string, n int) string { + for ; n > 0; n-- { + str = url.QueryEscape(str) + } + return str[:] +} diff --git a/adapters/vastbidder/macro_processor_test.go b/adapters/vastbidder/macro_processor_test.go new file mode 100644 index 00000000000..2b2b513df2c --- /dev/null +++ b/adapters/vastbidder/macro_processor_test.go @@ -0,0 +1,587 @@ +package vastbidder + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/stretchr/testify/assert" +) + +func getBidRequest(requestJSON string) *openrtb2.BidRequest { + bidRequest := &openrtb2.BidRequest{} + json.Unmarshal([]byte(requestJSON), bidRequest) + return bidRequest +} +func TestMacroProcessor_ProcessString(t *testing.T) { + testMacroValues := map[string]string{ + MacroPubID: `pubID`, + MacroTagID: `tagid value`, + MacroTagID + macroEscapeSuffix: `tagid+value`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroValues[MacroPubID], + }, + }, + } + + type fields struct { + bidRequest *openrtb2.BidRequest + } + tests := []struct { + name string + in string + expected string + }{ + { + name: "Empty Input", + in: "", + expected: "", + }, + { + name: "No Macro Replacement", + in: "Hello Test No Macro", + expected: "Hello Test No Macro", + }, + { + name: "Start Macro", + in: GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "End Macro", + in: "HELLO" + GetMacroKey(MacroTagID), + expected: "HELLO" + testMacroValues[MacroTagID], + }, + { + name: "Start-End Macro", + in: GetMacroKey(MacroTagID) + "HELLO" + GetMacroKey(MacroTagID), + expected: testMacroValues[MacroTagID] + "HELLO" + testMacroValues[MacroTagID], + }, + { + name: "Half Start Macro", + in: macroPrefix + GetMacroKey(MacroTagID) + "HELLO", + expected: macroPrefix + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "Half End Macro", + in: "HELLO" + GetMacroKey(MacroTagID) + macroSuffix, + expected: "HELLO" + testMacroValues[MacroTagID] + macroSuffix, + }, + { + name: "Concatenated Macro", + in: GetMacroKey(MacroTagID) + GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "Incomplete Concatenation Macro", + in: GetMacroKey(MacroTagID) + macroSuffix + "LINKHELLO", + expected: testMacroValues[MacroTagID] + macroSuffix + "LINKHELLO", + }, + { + name: "Concatenation with Suffix Macro", + in: GetMacroKey(MacroTagID) + macroPrefix + GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + macroPrefix + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "Unknown Macro", + in: GetMacroKey(`UNKNOWN`) + `ABC`, + expected: GetMacroKey(`UNKNOWN`) + `ABC`, + }, + { + name: "Incomplete macro suffix", + in: "START" + macroSuffix, + expected: "START" + macroSuffix, + }, + { + name: "Incomplete Start and End", + in: string(macroPrefix[0]) + GetMacroKey(MacroTagID) + " Value " + GetMacroKey(MacroTagID) + string(macroSuffix[0]), + expected: string(macroPrefix[0]) + testMacroValues[MacroTagID] + " Value " + testMacroValues[MacroTagID] + string(macroSuffix[0]), + }, + { + name: "Special Character", + in: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + GetMacroKey(MacroTagID) + "\" Data", + expected: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + testMacroValues[MacroTagID] + "\" Data", + }, + { + name: "Empty Value", + in: GetMacroKey(MacroTimeout) + "Hello", + expected: "Hello", + }, + { + name: "EscapingMacræo", + in: GetMacroKey(MacroTagID), + expected: testMacroValues[MacroTagID], + }, + { + name: "SingleEscapingMacro", + in: GetMacroKey(MacroTagID + macroEscapeSuffix), + expected: testMacroValues[MacroTagID+macroEscapeSuffix], + }, + { + name: "DoubleEscapingMacro", + in: GetMacroKey(MacroTagID + macroEscapeSuffix + macroEscapeSuffix), + expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + }, + + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //Init Bidder Macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + gotResponse := mp.ProcessString(tt.in) + assert.Equal(t, tt.expected, gotResponse) + }) + } +} + +func TestMacroProcessor_processKey(t *testing.T) { + testMacroValues := map[string]string{ + MacroPubID: `pub id`, + MacroPubID + macroEscapeSuffix: `pub+id`, + MacroTagID: `tagid value`, + MacroTagID + macroEscapeSuffix: `tagid+value`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroValues[MacroPubID], + }, + }, + } + type args struct { + cache map[string]string + key string + } + type want struct { + expected string + ok bool + cache map[string]string + } + tests := []struct { + name string + args args + want want + }{ + { + name: `emptyKey`, + args: args{}, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `cachedKeyFound`, + args: args{ + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + key: MacroPubID, + }, + want: want{ + expected: testMacroValues[MacroPubID], + ok: true, + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + }, + }, + { + name: `valueFound`, + args: args{ + key: MacroTagID, + }, + want: want{ + expected: testMacroValues[MacroTagID], + ok: true, + cache: map[string]string{}, + }, + }, + { + name: `2TimesEscaping`, + args: args{ + key: MacroTagID + macroEscapeSuffix + macroEscapeSuffix, + }, + want: want{ + expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + ok: true, + cache: map[string]string{}, + }, + }, + { + name: `macroNotPresent`, + args: args{ + key: `Unknown`, + }, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `macroNotPresentInEscaping`, + args: args{ + key: `Unknown` + macroEscapeSuffix, + }, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `cachedKey`, + args: args{ + key: MacroPubID, + }, + want: want{ + expected: testMacroValues[MacroPubID], + ok: true, + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + }, + }, + { + name: `cachedEscapingKey`, + args: args{ + key: MacroPubID + macroEscapeSuffix, + }, + want: want{ + expected: testMacroValues[MacroPubID+macroEscapeSuffix], + ok: true, + cache: map[string]string{ + MacroPubID + macroEscapeSuffix: testMacroValues[MacroPubID+macroEscapeSuffix], + }, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + //init cache of macro processor + if nil != tt.args.cache { + mp.macroCache = tt.args.cache + } + + actual, ok := mp.processKey(tt.args.key) + assert.Equal(t, tt.want.expected, actual) + assert.Equal(t, tt.want.ok, ok) + assert.Equal(t, tt.want.cache, mp.macroCache) + }) + } +} + +func TestMacroProcessor_processURLValues(t *testing.T) { + testMacroValues := map[string]string{ + MacroPubID: `pub id`, + MacroPubID + macroEscapeSuffix: `pub+id`, + MacroTagID: `tagid value`, + MacroTagID + macroEscapeSuffix: `tagid+value`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroValues[MacroPubID], + }, + }, + } + type args struct { + values url.Values + flags Flags + } + tests := []struct { + name string + args args + want url.Values + }{ + { + name: `AllEmptyParamsRemovedEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubName)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroPubName)}, + }, + flags: Flags{ + RemoveEmptyParam: true, + }, + }, + want: url.Values{}, + }, + { + name: `AllEmptyParamsKeepEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubName)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroPubName)}, + }, + flags: Flags{ + RemoveEmptyParam: false, + }, + }, + want: url.Values{ + `k1`: []string{""}, + `k2`: []string{""}, + `k3`: []string{""}, + }, + }, + { + name: `MixedParamsRemoveEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubID)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroTagID)}, + }, + flags: Flags{ + RemoveEmptyParam: true, + }, + }, + want: url.Values{ + `k1`: []string{testMacroValues[MacroPubID]}, + `k3`: []string{testMacroValues[MacroTagID]}, + }, + }, + { + name: `MixedParamsKeepEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubID)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroTagID)}, + `k4`: []string{`UNKNOWN`}, + `k5`: []string{GetMacroKey(`UNKNOWN`)}, + }, + flags: Flags{ + RemoveEmptyParam: false, + }, + }, + want: url.Values{ + `k1`: []string{testMacroValues[MacroPubID]}, + `k2`: []string{""}, + `k3`: []string{testMacroValues[MacroTagID]}, + `k4`: []string{`UNKNOWN`}, + `k5`: []string{GetMacroKey(`UNKNOWN`)}, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + actual := mp.processURLValues(tt.args.values, tt.args.flags) + + actualValues, _ := url.ParseQuery(actual) + assert.Equal(t, tt.want, actualValues) + }) + } +} + +func TestMacroProcessor_processURLValuesEscapingKeys(t *testing.T) { + testMacroImpValues := map[string]string{ + MacroPubID: `pub id`, + MacroTagID: `tagid value`, + } + + testMacroValues := map[string]string{ + MacroPubID: `pub+id`, + MacroTagID: `tagid+value`, + MacroTagID + macroEscapeSuffix: `tagid%2Bvalue`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%252Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroImpValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroImpValues[MacroPubID], + }, + }, + } + type args struct { + key string + value string + } + tests := []struct { + name string + args args + want string + }{ + { + name: `EmptyKeyValue`, + args: args{}, + want: ``, + }, + { + name: `WithoutEscaping`, + args: args{key: `k1`, value: GetMacroKey(MacroTagID)}, + want: `k1=` + testMacroValues[MacroTagID], + }, + { + name: `WithEscaping`, + args: args{key: `k1`, value: GetMacroKey(MacroTagID + macroEscapeSuffix)}, + want: `k1=` + testMacroValues[MacroTagID+macroEscapeSuffix], + }, + { + name: `With2LevelEscaping`, + args: args{key: `k1`, value: GetMacroKey(MacroTagID + macroEscapeSuffix + macroEscapeSuffix)}, + want: `k1=` + testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + values := url.Values{} + if len(tt.args.key) > 0 { + values.Add(tt.args.key, tt.args.value) + } + + actual := mp.processURLValues(values, Flags{}) + assert.Equal(t, tt.want, actual) + }) + } +} + +func TestMacroProcessor_ProcessURL(t *testing.T) { + testMacroImpValues := map[string]string{ + MacroPubID: `123`, + MacroSiteID: `567`, + MacroTagID: `tagid value`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroImpValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + ID: testMacroImpValues[MacroSiteID], + Publisher: &openrtb2.Publisher{ + ID: testMacroImpValues[MacroPubID], + }, + }, + } + + type args struct { + uri string + flags Flags + } + tests := []struct { + name string + args args + wantResponse string + }{ + { + name: "EmptyURI", + args: args{ + uri: ``, + flags: Flags{RemoveEmptyParam: true}, + }, + wantResponse: ``, + }, + { + name: "RemovedEmptyParams1", + args: args{ + uri: `http://xyz.domain.com/` + GetMacroKey(MacroPubID) + `/` + GetMacroKey(MacroSiteID) + `?tagID=` + GetMacroKey(MacroTagID) + `¬found=` + GetMacroKey(MacroTimeout) + `&k1=v1&k2=v2`, + flags: Flags{RemoveEmptyParam: true}, + }, + wantResponse: `http://xyz.domain.com/123/567?tagID=tagid+value&k1=v1&k2=v2`, + }, + { + name: "RemovedEmptyParams2", + args: args{ + uri: `http://xyz.domain.com/` + GetMacroKey(MacroPubID) + `/` + GetMacroKey(MacroSiteID) + `?tagID=` + GetMacroKey(MacroTagID+macroEscapeSuffix) + `¬found=` + GetMacroKey(MacroTimeout) + `&k1=v1&k2=v2`, + flags: Flags{RemoveEmptyParam: false}, + }, + wantResponse: `http://xyz.domain.com/123/567?tagID=tagid+value¬found=&k1=v1&k2=v2`, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + gotResponse := mp.ProcessURL(tt.args.uri, tt.args.flags) + assertURL(t, tt.wantResponse, gotResponse) + }) + } +} + +func assertURL(t *testing.T, expected, actual string) { + actualURL, _ := url.Parse(actual) + expectedURL, _ := url.Parse(expected) + + if nil == actualURL || nil == expectedURL { + assert.True(t, (nil == actualURL) == (nil == expectedURL), `actual or expected url parsing failed`) + } else { + assert.Equal(t, expectedURL.Scheme, actualURL.Scheme) + assert.Equal(t, expectedURL.Opaque, actualURL.Opaque) + assert.Equal(t, expectedURL.User, actualURL.User) + assert.Equal(t, expectedURL.Host, actualURL.Host) + assert.Equal(t, expectedURL.Path, actualURL.Path) + assert.Equal(t, expectedURL.RawPath, actualURL.RawPath) + assert.Equal(t, expectedURL.ForceQuery, actualURL.ForceQuery) + assert.Equal(t, expectedURL.Query(), actualURL.Query()) + assert.Equal(t, expectedURL.Fragment, actualURL.Fragment) + } +} diff --git a/adapters/vastbidder/mapper.go b/adapters/vastbidder/mapper.go new file mode 100644 index 00000000000..cbbfe34c119 --- /dev/null +++ b/adapters/vastbidder/mapper.go @@ -0,0 +1,180 @@ +package vastbidder + +type macroCallBack struct { + cached bool + callback func(IBidderMacro, string) string +} + +//Mapper will map macro with its respective call back function +type Mapper map[string]*macroCallBack + +func (obj Mapper) clone() Mapper { + cloned := make(Mapper, len(obj)) + for k, v := range obj { + newCallback := *v + cloned[k] = &newCallback + } + return cloned +} + +var _defaultMapper = Mapper{ + //Request + MacroTest: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTest}, + MacroTimeout: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTimeout}, + MacroWhitelistSeat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistSeat}, + MacroWhitelistLang: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistLang}, + MacroBlockedSeat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedSeat}, + MacroCurrency: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCurrency}, + MacroBlockedCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedCategory}, + MacroBlockedAdvertiser: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedAdvertiser}, + MacroBlockedApp: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedApp}, + + //Source + MacroFD: ¯oCallBack{cached: true, callback: IBidderMacro.MacroFD}, + MacroTransactionID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTransactionID}, + MacroPaymentIDChain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPaymentIDChain}, + + //Regs + MacroCoppa: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCoppa}, + + //Impression + MacroDisplayManager: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManager}, + MacroDisplayManagerVersion: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManagerVersion}, + MacroInterstitial: ¯oCallBack{cached: false, callback: IBidderMacro.MacroInterstitial}, + MacroTagID: ¯oCallBack{cached: false, callback: IBidderMacro.MacroTagID}, + MacroBidFloor: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloor}, + MacroBidFloorCurrency: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloorCurrency}, + MacroSecure: ¯oCallBack{cached: false, callback: IBidderMacro.MacroSecure}, + MacroPMP: ¯oCallBack{cached: false, callback: IBidderMacro.MacroPMP}, + + //Video + MacroVideoMIMES: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMIMES}, + MacroVideoMinimumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumDuration}, + MacroVideoMaximumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumDuration}, + MacroVideoProtocols: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoProtocols}, + MacroVideoPlayerWidth: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerWidth}, + MacroVideoPlayerHeight: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerHeight}, + MacroVideoStartDelay: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoStartDelay}, + MacroVideoPlacement: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlacement}, + MacroVideoLinearity: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoLinearity}, + MacroVideoSkip: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkip}, + MacroVideoSkipMinimum: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipMinimum}, + MacroVideoSkipAfter: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipAfter}, + MacroVideoSequence: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSequence}, + MacroVideoBlockedAttribute: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBlockedAttribute}, + MacroVideoMaximumExtended: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumExtended}, + MacroVideoMinimumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumBitRate}, + MacroVideoMaximumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumBitRate}, + MacroVideoBoxing: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBoxing}, + MacroVideoPlaybackMethod: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlaybackMethod}, + MacroVideoDelivery: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoDelivery}, + MacroVideoPosition: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPosition}, + MacroVideoAPI: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoAPI}, + + //Site + MacroSiteID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteID}, + MacroSiteName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteName}, + MacroSitePage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSitePage}, + MacroSiteReferrer: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteReferrer}, + MacroSiteSearch: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteSearch}, + MacroSiteMobile: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteMobile}, + + //App + MacroAppID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppID}, + MacroAppName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppName}, + MacroAppBundle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppBundle}, + MacroAppStoreURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppStoreURL}, + MacroAppVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppVersion}, + MacroAppPaid: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppPaid}, + + //SiteAppCommon + MacroCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCategory}, + MacroDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDomain}, + MacroSectionCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSectionCategory}, + MacroPageCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPageCategory}, + MacroPrivacyPolicy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPrivacyPolicy}, + MacroKeywords: ¯oCallBack{cached: true, callback: IBidderMacro.MacroKeywords}, + + //Publisher + MacroPubID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubID}, + MacroPubName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubName}, + MacroPubDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubDomain}, + + //Content + MacroContentID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentID}, + MacroContentEpisode: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEpisode}, + MacroContentTitle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentTitle}, + MacroContentSeries: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeries}, + MacroContentSeason: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeason}, + MacroContentArtist: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentArtist}, + MacroContentGenre: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentGenre}, + MacroContentAlbum: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentAlbum}, + MacroContentISrc: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentISrc}, + MacroContentURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentURL}, + MacroContentCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentCategory}, + MacroContentProductionQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentProductionQuality}, + MacroContentVideoQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentVideoQuality}, + MacroContentContext: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentContext}, + MacroContentContentRating: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentContentRating}, + MacroContentUserRating: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentUserRating}, + MacroContentQAGMediaRating: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentQAGMediaRating}, + MacroContentKeywords: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentKeywords}, + MacroContentLiveStream: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLiveStream}, + MacroContentSourceRelationship: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSourceRelationship}, + MacroContentLength: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLength}, + MacroContentLanguage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLanguage}, + MacroContentEmbeddable: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEmbeddable}, + + //Producer + MacroProducerID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerID}, + MacroProducerName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerName}, + + //Device + MacroUserAgent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserAgent}, + MacroDNT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDNT}, + MacroLMT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLMT}, + MacroIP: ¯oCallBack{cached: true, callback: IBidderMacro.MacroIP}, + MacroDeviceType: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceType}, + MacroMake: ¯oCallBack{cached: true, callback: IBidderMacro.MacroMake}, + MacroModel: ¯oCallBack{cached: true, callback: IBidderMacro.MacroModel}, + MacroDeviceOS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOS}, + MacroDeviceOSVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOSVersion}, + MacroDeviceWidth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceWidth}, + MacroDeviceHeight: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceHeight}, + MacroDeviceJS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceJS}, + MacroDeviceLanguage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceLanguage}, + MacroDeviceIFA: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceIFA}, + MacroDeviceDIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDSHA1}, + MacroDeviceDIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDMD5}, + MacroDeviceDPIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDSHA1}, + MacroDeviceDPIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDMD5}, + MacroDeviceMACSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACSHA1}, + MacroDeviceMACMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACMD5}, + + //Geo + MacroLatitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLatitude}, + MacroLongitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLongitude}, + MacroCountry: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCountry}, + MacroRegion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroRegion}, + MacroCity: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCity}, + MacroZip: ¯oCallBack{cached: true, callback: IBidderMacro.MacroZip}, + MacroUTCOffset: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUTCOffset}, + + //User + MacroUserID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserID}, + MacroYearOfBirth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroYearOfBirth}, + MacroGender: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGender}, + + //Extension + MacroGDPRConsent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPRConsent}, + MacroGDPR: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPR}, + MacroUSPrivacy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUSPrivacy}, + + //Additional + MacroCacheBuster: ¯oCallBack{cached: false, callback: IBidderMacro.MacroCacheBuster}, +} + +//GetDefaultMapper will return clone of default Mapper function +func GetDefaultMapper() Mapper { + return _defaultMapper.clone() +} diff --git a/adapters/vastbidder/sample_spotx_macro.go.bak b/adapters/vastbidder/sample_spotx_macro.go.bak new file mode 100644 index 00000000000..8f3aafbdcc7 --- /dev/null +++ b/adapters/vastbidder/sample_spotx_macro.go.bak @@ -0,0 +1,28 @@ +package vastbidder + +import ( + "github.com/prebid/prebid-server/openrtb_ext" +) + +//SpotxMacro default implementation +type SpotxMacro struct { + *BidderMacro +} + +//NewSpotxMacro contains definition for all openrtb macro's +func NewSpotxMacro() IBidderMacro { + obj := &SpotxMacro{ + BidderMacro: &BidderMacro{}, + } + obj.IBidderMacro = obj + return obj +} + +//GetBidderKeys will set bidder level keys +func (tag *SpotxMacro) GetBidderKeys() map[string]string { + return NormalizeJSON(tag.ImpBidderExt) +} + +func init() { + RegisterNewBidderMacro(openrtb_ext.BidderSpotX, NewSpotxMacro) +} diff --git a/adapters/vastbidder/tagbidder.go b/adapters/vastbidder/tagbidder.go new file mode 100644 index 00000000000..4b7e5efc82f --- /dev/null +++ b/adapters/vastbidder/tagbidder.go @@ -0,0 +1,87 @@ +package vastbidder + +import ( + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//TagBidder is default implementation of ITagBidder +type TagBidder struct { + adapters.Bidder + bidderName openrtb_ext.BidderName + adapterConfig *config.Adapter +} + +//MakeRequests will contains default definition for processing queries +func (a *TagBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + bidderMacro := GetNewBidderMacro(a.bidderName) + bidderMapper := GetDefaultMapper() + macroProcessor := NewMacroProcessor(bidderMacro, bidderMapper) + + //Setting config parameters + //bidderMacro.SetBidderConfig(a.bidderConfig) + bidderMacro.SetAdapterConfig(a.adapterConfig) + bidderMacro.InitBidRequest(request) + + requestData := []*adapters.RequestData{} + for impIndex := range request.Imp { + bidderExt, err := bidderMacro.LoadImpression(&request.Imp[impIndex]) + if nil != err { + continue + } + + //iterate each vast tags, and load vast tag + for vastTagIndex, tag := range bidderExt.Tags { + //load vasttag + bidderMacro.LoadVASTTag(tag) + + //Setting Bidder Level Keys + bidderKeys := bidderMacro.GetBidderKeys() + macroProcessor.SetBidderKeys(bidderKeys) + + uri := macroProcessor.ProcessURL(bidderMacro.GetURI(), Flags{RemoveEmptyParam: true}) + + // append custom headers if any + headers := bidderMacro.getAllHeaders() + + requestData = append(requestData, &adapters.RequestData{ + Params: &adapters.BidRequestParams{ + ImpIndex: impIndex, + VASTTagIndex: vastTagIndex, + }, + Method: `GET`, + Uri: uri, + Headers: headers, + }) + } + } + + return requestData, nil +} + +//MakeBids makes bids +func (a *TagBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + //response validation can be done here independently + //handler, err := GetResponseHandler(a.bidderConfig.ResponseType) + handler, err := GetResponseHandler(VASTTagHandlerType) + if nil != err { + return nil, []error{err} + } + return handler.MakeBids(internalRequest, externalRequest, response) +} + +//NewTagBidder is an constructor for TagBidder +func NewTagBidder(bidderName openrtb_ext.BidderName, config config.Adapter) *TagBidder { + obj := &TagBidder{ + bidderName: bidderName, + adapterConfig: &config, + } + return obj +} + +// Builder builds a new instance of the 33Across adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + return NewTagBidder(bidderName, config), nil +} diff --git a/adapters/vastbidder/tagbidder_test.go b/adapters/vastbidder/tagbidder_test.go new file mode 100644 index 00000000000..086e3a1ad3a --- /dev/null +++ b/adapters/vastbidder/tagbidder_test.go @@ -0,0 +1,149 @@ +package vastbidder + +import ( + "net/http" + "testing" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +//TestMakeRequests verifies +// 1. default and custom headers are set +func TestMakeRequests(t *testing.T) { + + type args struct { + customHeaders map[string]string + req *openrtb2.BidRequest + } + type want struct { + impIDReqHeaderMap map[string]http.Header + } + tests := []struct { + name string + args args + want want + }{ + { + name: "multi_impression_req", + args: args{ + customHeaders: map[string]string{ + "my-custom-header": "custom-value", + }, + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { // vast 2.0 + ID: "vast_2_0_imp_req", + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST20, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "vast_4_0_imp_req", + Video: &openrtb2.Video{ // vast 4.0 + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "vast_2_0_4_0_wrapper_imp_req", + Video: &openrtb2.Video{ // vast 2 and 4.0 wrapper + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40Wrapper, + openrtb2.ProtocolVAST20, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "other_non_vast_protocol", + Video: &openrtb2.Video{ // DAAST 1.0 + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolDAAST10, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + + ID: "no_protocol_field_set", + Video: &openrtb2.Video{ // vast 2 and 4.0 wrapper + Protocols: []openrtb2.Protocol{}, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + }, + }, + }, + want: want{ + impIDReqHeaderMap: map[string]http.Header{ + "vast_2_0_imp_req": { + "X-Forwarded-For": []string{"1.1.1.1"}, + "User-Agent": []string{"user-agent"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "vast_4_0_imp_req": { + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "vast_2_0_4_0_wrapper_imp_req": { + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "other_non_vast_protocol": { + "My-Custom-Header": []string{"custom-value"}, + }, // no default headers expected + "no_protocol_field_set": { // set all default headers + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderName := openrtb_ext.BidderName("myVastBidderMacro") + RegisterNewBidderMacro(bidderName, func() IBidderMacro { + return newMyVastBidderMacro(tt.args.customHeaders) + }) + bidder := NewTagBidder(bidderName, config.Adapter{}) + reqData, err := bidder.MakeRequests(tt.args.req, nil) + assert.Nil(t, err) + for _, req := range reqData { + impID := tt.args.req.Imp[req.Params.ImpIndex].ID + expectedHeaders := tt.want.impIDReqHeaderMap[impID] + assert.Equal(t, expectedHeaders, req.Headers, "test for - "+impID) + } + }) + } +} diff --git a/adapters/vastbidder/util.go b/adapters/vastbidder/util.go new file mode 100644 index 00000000000..8ad02535ec6 --- /dev/null +++ b/adapters/vastbidder/util.go @@ -0,0 +1,70 @@ +package vastbidder + +import ( + "bytes" + "encoding/json" + "fmt" + "math/rand" + "strconv" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func ObjectArrayToString(len int, separator string, cb func(i int) string) string { + if 0 == len { + return "" + } + + var out bytes.Buffer + for i := 0; i < len; i++ { + if out.Len() > 0 { + out.WriteString(separator) + } + out.WriteString(cb(i)) + } + return out.String() +} + +func readImpExt(impExt json.RawMessage) (*openrtb_ext.ExtImpVASTBidder, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(impExt, &bidderExt); err != nil { + return nil, err + } + + vastBidderExt := openrtb_ext.ExtImpVASTBidder{} + if err := json.Unmarshal(bidderExt.Bidder, &vastBidderExt); err != nil { + return nil, err + } + return &vastBidderExt, nil +} + +func normalizeObject(prefix string, out map[string]string, obj map[string]interface{}) { + for k, value := range obj { + key := k + if len(prefix) > 0 { + key = prefix + "." + k + } + + switch val := value.(type) { + case string: + out[key] = val + case []interface{}: //array + continue + case map[string]interface{}: //object + normalizeObject(key, out, val) + default: //all int, float + out[key] = fmt.Sprint(value) + } + } +} + +func NormalizeJSON(obj map[string]interface{}) map[string]string { + out := map[string]string{} + normalizeObject("", out, obj) + return out +} + +var GetRandomID = func() string { + return strconv.FormatInt(rand.Int63(), intBase) +} diff --git a/adapters/vastbidder/vast_tag_response_handler.go b/adapters/vastbidder/vast_tag_response_handler.go new file mode 100644 index 00000000000..f3436370854 --- /dev/null +++ b/adapters/vastbidder/vast_tag_response_handler.go @@ -0,0 +1,334 @@ +package vastbidder + +import ( + "encoding/json" + "errors" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/beevik/etree" + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +var durationRegExp = regexp.MustCompile(`^([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)(\.(\d{1,3}))?$`) + +//IVASTTagResponseHandler to parse VAST Tag +type IVASTTagResponseHandler interface { + ITagResponseHandler + ParseExtension(version string, tag *etree.Element, bid *adapters.TypedBid) []error + GetStaticPrice(ext json.RawMessage) float64 +} + +//VASTTagResponseHandler to parse VAST Tag +type VASTTagResponseHandler struct { + IVASTTagResponseHandler + ImpBidderExt *openrtb_ext.ExtImpVASTBidder + VASTTag *openrtb_ext.ExtImpVASTBidderTag +} + +//NewVASTTagResponseHandler returns new object +func NewVASTTagResponseHandler() *VASTTagResponseHandler { + obj := &VASTTagResponseHandler{} + obj.IVASTTagResponseHandler = obj + return obj +} + +//Validate will return bids +func (handler *VASTTagResponseHandler) Validate(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) []error { + if response.StatusCode != http.StatusOK { + return []error{errors.New(`validation failed`)} + } + + if len(internalRequest.Imp) < externalRequest.Params.ImpIndex { + return []error{errors.New(`validation failed invalid impression index`)} + } + + impExt, err := readImpExt(internalRequest.Imp[externalRequest.Params.ImpIndex].Ext) + if nil != err { + return []error{err} + } + + if len(impExt.Tags) < externalRequest.Params.VASTTagIndex { + return []error{errors.New(`validation failed invalid vast tag index`)} + } + + //Initialise Extensions + handler.ImpBidderExt = impExt + handler.VASTTag = impExt.Tags[externalRequest.Params.VASTTagIndex] + return nil +} + +//MakeBids will return bids +func (handler *VASTTagResponseHandler) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if err := handler.IVASTTagResponseHandler.Validate(internalRequest, externalRequest, response); len(err) > 0 { + return nil, err[:] + } + + bidResponses, err := handler.vastTagToBidderResponse(internalRequest, externalRequest, response) + return bidResponses, err +} + +//ParseExtension will parse VAST XML extension object +func (handler *VASTTagResponseHandler) ParseExtension(version string, ad *etree.Element, bid *adapters.TypedBid) []error { + return nil +} + +func (handler *VASTTagResponseHandler) vastTagToBidderResponse(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + doc := etree.NewDocument() + + //Read Document + if err := doc.ReadFromBytes(response.Body); err != nil { + errs = append(errs, err) + return nil, errs[:] + } + + //Check VAST Tag + vast := doc.Element.FindElement(`./VAST`) + if vast == nil { + errs = append(errs, errors.New("VAST Tag Not Found")) + return nil, errs[:] + } + + //Check VAST/Ad Tag + adElement := getAdElement(vast) + if nil == adElement { + errs = append(errs, errors.New("VAST/Ad Tag Not Found")) + return nil, errs[:] + } + + typedBid := &adapters.TypedBid{ + Bid: &openrtb2.Bid{}, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{ + VASTTagID: handler.VASTTag.TagID, + }, + } + + creatives := adElement.FindElements("Creatives/Creative") + if nil != creatives { + for _, creative := range creatives { + // get creative id + typedBid.Bid.CrID = getCreativeID(creative) + + // get duration from vast creative + dur, err := getDuration(creative) + if nil != err { + // get duration from input bidder vast tag + dur = getStaticDuration(handler.VASTTag) + } + if dur > 0 { + typedBid.BidVideo.Duration = int(dur) // prebid expects int value + } + } + } + + bidResponse := &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{typedBid}, + Currency: `USD`, //TODO: Need to check how to get currency value + } + + //GetVersion + version := vast.SelectAttrValue(`version`, `2.0`) + + if err := handler.IVASTTagResponseHandler.ParseExtension(version, adElement, typedBid); len(err) > 0 { + errs = append(errs, err...) + return nil, errs[:] + } + + //if bid.price is not set in ParseExtension + if typedBid.Bid.Price <= 0 { + price, currency := getPricingDetails(version, adElement) + if price <= 0 { + price, currency = getStaticPricingDetails(handler.VASTTag) + if price <= 0 { + errs = append(errs, &errortypes.NoBidPrice{Message: "Bid Price Not Present"}) + return nil, errs[:] + } + } + typedBid.Bid.Price = price + if len(currency) > 0 { + bidResponse.Currency = currency + } + } + + typedBid.Bid.ADomain = getAdvertisers(version, adElement) + + //if bid.id is not set in ParseExtension + if len(typedBid.Bid.ID) == 0 { + typedBid.Bid.ID = GetRandomID() + } + + //if bid.impid is not set in ParseExtension + if len(typedBid.Bid.ImpID) == 0 { + typedBid.Bid.ImpID = internalRequest.Imp[externalRequest.Params.ImpIndex].ID + } + + //if bid.adm is not set in ParseExtension + if len(typedBid.Bid.AdM) == 0 { + typedBid.Bid.AdM = string(response.Body) + } + + //if bid.CrID is not set in ParseExtension + if len(typedBid.Bid.CrID) == 0 { + typedBid.Bid.CrID = "cr_" + GetRandomID() + } + + return bidResponse, nil +} + +func getAdElement(vast *etree.Element) *etree.Element { + if ad := vast.FindElement(`./Ad/Wrapper`); nil != ad { + return ad + } + if ad := vast.FindElement(`./Ad/InLine`); nil != ad { + return ad + } + return nil +} + +func getAdvertisers(vastVer string, ad *etree.Element) []string { + version, err := strconv.ParseFloat(vastVer, 64) + if err != nil { + version = 2.0 + } + + advertisers := make([]string, 0) + + switch int(version) { + case 2, 3: + for _, ext := range ad.FindElements(`./Extensions/Extension/`) { + for _, attr := range ext.Attr { + if attr.Key == "type" && attr.Value == "advertiser" { + for _, ele := range ext.ChildElements() { + if ele.Tag == "Advertiser" { + if strings.TrimSpace(ele.Text()) != "" { + advertisers = append(advertisers, ele.Text()) + } + } + } + } + } + } + case 4: + if ad.FindElement("./Advertiser") != nil { + adv := strings.TrimSpace(ad.FindElement("./Advertiser").Text()) + if adv != "" { + advertisers = append(advertisers, adv) + } + } + default: + glog.V(3).Infof("Handle getAdvertisers for VAST version %d", int(version)) + } + + if len(advertisers) == 0 { + return nil + } + return advertisers +} + +func getStaticPricingDetails(vastTag *openrtb_ext.ExtImpVASTBidderTag) (float64, string) { + if nil == vastTag { + return 0.0, "" + } + return vastTag.Price, "USD" +} + +func getPricingDetails(version string, ad *etree.Element) (float64, string) { + var currency string + var node *etree.Element + + if `2.0` == version { + node = ad.FindElement(`./Extensions/Extension/Price`) + } else { + node = ad.FindElement(`./Pricing`) + } + + if nil == node { + return 0.0, currency + } + + priceValue, err := strconv.ParseFloat(node.Text(), 64) + if nil != err { + return 0.0, currency + } + + currencyNode := node.SelectAttr(`currency`) + if nil != currencyNode { + currency = currencyNode.Value + } + + return priceValue, currency +} + +// getDuration extracts the duration of the bid from input creative of Linear type. +// The lookup may vary from vast version provided in the input +// returns duration in seconds or error if failed to obtained the duration. +// If multple Linear tags are present, onlyfirst one will be used +// +// It will lookup for duration only in case of creative type is Linear. +// If creative type other than Linear then this function will return error +// For Linear Creative it will lookup for Duration attribute.Duration value will be in hh:mm:ss.mmm format as per VAST specifications +// If Duration attribute not present this will return error +// +// After extracing the duration it will convert it into seconds +// +// The ad server uses the element to denote +// the intended playback duration for the video or audio component of the ad. +// Time value may be in the format HH:MM:SS.mmm where .mmm indicates milliseconds. +// Providing milliseconds is optional. +// +// Reference +// 1.https://iabtechlab.com/wp-content/uploads/2019/06/VAST_4.2_final_june26.pdf +// 2.https://iabtechlab.com/wp-content/uploads/2018/11/VAST4.1-final-Nov-8-2018.pdf +// 3.https://iabtechlab.com/wp-content/uploads/2016/05/VAST4.0_Updated_April_2016.pdf +// 4.https://iabtechlab.com/wp-content/uploads/2016/04/VASTv3_0.pdf +func getDuration(creative *etree.Element) (int, error) { + if nil == creative { + return 0, errors.New("Invalid Creative") + } + node := creative.FindElement("./Linear/Duration") + if nil == node { + return 0, errors.New("Invalid Duration") + } + duration := node.Text() + // check if milliseconds is provided + match := durationRegExp.FindStringSubmatch(duration) + if nil == match { + return 0, errors.New("Invalid Duration") + } + repl := "${1}h${2}m${3}s" + ms := match[5] + if "" != ms { + repl += "${5}ms" + } + duration = durationRegExp.ReplaceAllString(duration, repl) + dur, err := time.ParseDuration(duration) + if err != nil { + return 0, err + } + return int(dur.Seconds()), nil +} + +func getStaticDuration(vastTag *openrtb_ext.ExtImpVASTBidderTag) int { + if nil == vastTag { + return 0 + } + return vastTag.Duration +} + +//getCreativeID looks for ID inside input creative tag +func getCreativeID(creative *etree.Element) string { + if nil == creative { + return "" + } + return creative.SelectAttrValue("id", "") +} diff --git a/adapters/vastbidder/vast_tag_response_handler_test.go b/adapters/vastbidder/vast_tag_response_handler_test.go new file mode 100644 index 00000000000..28c29ef6776 --- /dev/null +++ b/adapters/vastbidder/vast_tag_response_handler_test.go @@ -0,0 +1,385 @@ +package vastbidder + +import ( + "errors" + "fmt" + "sort" + "testing" + + "github.com/beevik/etree" + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) { + type args struct { + internalRequest *openrtb2.BidRequest + externalRequest *adapters.RequestData + response *adapters.ResponseData + vastTag *openrtb_ext.ExtImpVASTBidderTag + } + type want struct { + bidderResponse *adapters.BidderResponse + err []error + } + tests := []struct { + name string + args args + want want + }{ + { + name: `InlinePricingNode`, + args: args{ + internalRequest: &openrtb2.BidRequest{ + ID: `request_id_1`, + Imp: []openrtb2.Imp{ + { + ID: `imp_id_1`, + }, + }, + }, + externalRequest: &adapters.RequestData{ + Params: &adapters.BidRequestParams{ + ImpIndex: 0, + }, + }, + response: &adapters.ResponseData{ + Body: []byte(` `), + }, + vastTag: &openrtb_ext.ExtImpVASTBidderTag{ + TagID: "101", + Duration: 15, + }, + }, + want: want{ + bidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: `1234`, + ImpID: `imp_id_1`, + Price: 0.05, + AdM: ` `, + CrID: "cr_1234", + }, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{ + VASTTagID: "101", + Duration: 15, + }, + }, + }, + Currency: `USD`, + }, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := NewVASTTagResponseHandler() + GetRandomID = func() string { + return `1234` + } + handler.VASTTag = tt.args.vastTag + + bidderResponse, err := handler.vastTagToBidderResponse(tt.args.internalRequest, tt.args.externalRequest, tt.args.response) + assert.Equal(t, tt.want.bidderResponse, bidderResponse) + assert.Equal(t, tt.want.err, err) + }) + } +} + +//TestGetDurationInSeconds ... +// hh:mm:ss.mmm => 3:40:43.5 => 3 hours, 40 minutes, 43 seconds and 5 milliseconds +// => 3*60*60 + 40*60 + 43 + 5*0.001 => 10800 + 2400 + 43 + 0.005 => 13243.005 +func TestGetDurationInSeconds(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + duration int // seconds (will converted from string with format as HH:MM:SS.mmm) + err error + } + tests := []struct { + name string + args args + want want + }{ + // duration validation tests + {name: "duration 00:00:25 (= 25 seconds)", want: want{duration: 25}, args: args{creativeTag: ` 00:00:25 `}}, + {name: "duration 00:00:-25 (= -25 seconds)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:-25 `}}, + {name: "duration 00:00:30.999 (= 30.990 seconds (int -> 30 seconds))", want: want{duration: 30}, args: args{creativeTag: ` 00:00:30.999 `}}, + {name: "duration 00:01:08 (1 min 8 seconds = 68 seconds)", want: want{duration: 68}, args: args{creativeTag: ` 00:01:08 `}}, + {name: "duration 02:13:12 (2 hrs 13 min 12 seconds) = 7992 seconds)", want: want{duration: 7992}, args: args{creativeTag: ` 02:13:12 `}}, + {name: "duration 3:40:43.5 (3 hrs 40 min 43 seconds 5 ms) = 6043.005 seconds (int -> 6043 seconds))", want: want{duration: 13243}, args: args{creativeTag: ` 3:40:43.5 `}}, + {name: "duration 00:00:25.0005458 (0 hrs 0 min 25 seconds 0005458 ms) - invalid max ms is 999", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:25.0005458 `}}, + {name: "invalid duration 3:13:900 (3 hrs 13 min 900 seconds) = Invalid seconds )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:900 `}}, + {name: "invalid duration 3:13:34:44 (3 hrs 13 min 34 seconds :44=invalid) = ?? )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:34:44 `}}, + {name: "duration = 0:0:45.038 , with milliseconds duration (0 hrs 0 min 45 seconds and 038 millseconds) = 45.038 seconds (int -> 45 seconds) )", want: want{duration: 45}, args: args{creativeTag: ` 0:0:45.038 `}}, + {name: "duration = 0:0:48.50 = 48.050 seconds (int -> 48 seconds))", want: want{duration: 48}, args: args{creativeTag: ` 0:0:48.50 `}}, + {name: "duration = 0:0:28.59 = 28.059 seconds (int -> 28 seconds))", want: want{duration: 28}, args: args{creativeTag: ` 0:0:28.59 `}}, + {name: "duration = 56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56 `}}, + {name: "duration = :56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56 `}}, + {name: "duration = :56: (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56: `}}, + {name: "duration = ::56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` ::56 `}}, + {name: "duration = 56.445 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56.445 `}}, + {name: "duration = a:b:c.d (no numbers)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` a:b:c.d `}}, + + // tag validations tests + {name: "Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Companion Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Non-Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Invalid Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ``}}, + {name: "Nil Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ""}}, + + // multiple linear tags in creative + {name: "Multiple Linear Ads within Creative", want: want{duration: 25}, args: args{creativeTag: `0:0:250:0:30`}}, + // Case sensitivity check - passing DURATION (vast is case-sensitive as per https://vastvalidator.iabtechlab.com/dash) + {name: " all caps", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `0:0:10`}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + dur, err := getDuration(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.duration, dur) + assert.Equal(t, tt.want.err, err) + // if error expects 0 value for duration + if nil != err { + assert.Equal(t, 0, dur) + } + }) + } +} + +func BenchmarkGetDuration(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` 0:0:56.3 `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getDuration(creative) + } +} + +func TestGetCreativeId(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + id string + } + tests := []struct { + name string + args args + want want + }{ + {name: "creative tag with id", want: want{id: "233ff44"}, args: args{creativeTag: ``}}, + {name: "creative tag without id", want: want{id: ""}, args: args{creativeTag: ``}}, + {name: "no creative tag", want: want{id: ""}, args: args{creativeTag: ""}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + id := getCreativeID(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.id, id) + }) + } +} + +func BenchmarkGetCreativeID(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getCreativeID(creative) + } +} + +func TestGetAdvertisers(t *testing.T) { + tt := []struct { + name string + vastStr string + expected []string + }{ + { + name: "vast_4_with_advertiser", + vastStr: ` + + + www.iabtechlab.com + + + `, + expected: []string{"www.iabtechlab.com"}, + }, + { + name: "vast_4_without_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_4_with_empty_advertiser", + vastStr: ` + + + + + + `, + expected: []string{}, + }, + { + name: "vast_2_with_single_advertiser", + vastStr: ` + + + + + google.com + + + + + `, + expected: []string{"google.com"}, + }, + { + name: "vast_2_with_two_advertiser", + vastStr: ` + + + + + google.com + + + facebook.com + + + + + `, + expected: []string{"google.com", "facebook.com"}, + }, + { + name: "vast_2_with_no_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_2_with_epmty_advertiser", + vastStr: ` + + + + + + + + + + `, + expected: []string{}, + }, + { + name: "vast_3_with_single_advertiser", + vastStr: ` + + + + + google.com + + + + + `, + expected: []string{"google.com"}, + }, + { + name: "vast_3_with_two_advertiser", + vastStr: ` + + + + + google.com + + + facebook.com + + + + + `, + expected: []string{"google.com", "facebook.com"}, + }, + { + name: "vast_3_with_no_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_3_with_epmty_advertiser", + vastStr: ` + + + + + + + + + + `, + expected: []string{}, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + + doc := etree.NewDocument() + if err := doc.ReadFromString(tc.vastStr); err != nil { + t.Errorf("Failed to create etree doc from string %+v", err) + } + + vastDoc := doc.FindElement("./VAST") + vastVer := vastDoc.SelectAttrValue(`version`, `2.0`) + + ad := getAdElement(vastDoc) + + result := getAdvertisers(vastVer, ad) + + sort.Strings(result) + sort.Strings(tc.expected) + + if !assert.Equal(t, len(tc.expected), len(result), fmt.Sprintf("Expected slice length - %+v \nResult slice length - %+v", len(tc.expected), len(result))) { + return + } + + for i, expected := range tc.expected { + assert.Equal(t, expected, result[i], fmt.Sprintf("Element mismatch at position %d.\nExpected - %s\nActual - %s", i, expected, result[i])) + } + }) + } +} diff --git a/config/config.go b/config/config.go index 33855cb1f43..83001b10f3c 100755 --- a/config/config.go +++ b/config/config.go @@ -650,6 +650,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUcfunnel, "https://sync.aralego.com/idsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&usprivacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ducfunnel%26uid%3DSspCookieUserId") // openrtb_ext.BidderUnicorn doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUnruly, "https://sync.1rx.io/usersync2/rmpssp?sub=openwrap&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dunruly%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + // openrtb_ext.BidderVASTBidder doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderValueImpression, "https://rtb.valueimpression.com/usersync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dvalueimpression%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderVisx, "https://t.visx.net/s2s_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dvisx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") // openrtb_ext.BidderVrtcal doesn't have a good default. @@ -920,6 +921,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.unicorn.endpoint", "https://ds.uncn.jp/pb/0/bid.json") v.SetDefault("adapters.unruly.endpoint", "http://targeting.unrulymedia.com/openrtb/2.2") v.SetDefault("adapters.valueimpression.endpoint", "https://rtb.valueimpression.com/endpoint") + v.SetDefault("adapters.vastbidder.endpoint", "https://test.com") v.SetDefault("adapters.verizonmedia.disabled", true) v.SetDefault("adapters.visx.endpoint", "https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard") v.SetDefault("adapters.vrtcal.endpoint", "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1812") diff --git a/endpoints/events/vtrack.go b/endpoints/events/vtrack.go index b58d758d877..d547158b6c3 100644 --- a/endpoints/events/vtrack.go +++ b/endpoints/events/vtrack.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/PubMatic-OpenWrap/etree" + "github.com/beevik/etree" "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/openrtb_ext" @@ -563,6 +563,5 @@ func getDomain(site *openrtb2.Site) string { hostname = pageURL.Host } } - return hostname } diff --git a/endpoints/events/vtrack_test.go b/endpoints/events/vtrack_test.go index 0980843e650..6f290b22499 100644 --- a/endpoints/events/vtrack_test.go +++ b/endpoints/events/vtrack_test.go @@ -11,9 +11,8 @@ import ( "strings" "testing" - "github.com/PubMatic-OpenWrap/etree" + "github.com/beevik/etree" "github.com/mxmCherry/openrtb/v15/openrtb2" - "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/stored_requests" diff --git a/endpoints/openrtb2/ctv/types/adpod_types.go b/endpoints/openrtb2/ctv/types/adpod_types.go index 6e46929547d..275a01c2fbc 100644 --- a/endpoints/openrtb2/ctv/types/adpod_types.go +++ b/endpoints/openrtb2/ctv/types/adpod_types.go @@ -53,8 +53,10 @@ type ImpAdPodConfig struct { //ImpData example type ImpData struct { //AdPodGenerator - VideoExt *openrtb_ext.ExtVideoAdPod `json:"vidext,omitempty"` - Config []*ImpAdPodConfig `json:"imp,omitempty"` - ErrorCode *int `json:"ec,omitempty"` - Bid *AdPodBid `json:"-"` + ImpID string `json:"-"` + VideoExt *openrtb_ext.ExtVideoAdPod `json:"vidext,omitempty"` + Config []*ImpAdPodConfig `json:"imp,omitempty"` + ErrorCode *int `json:"ec,omitempty"` + BlockedVASTTags map[string][]string `json:"blockedtags,omitempty"` + Bid *AdPodBid `json:"-"` } diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index c8f5a32e307..a2299517695 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/prebid/prebid-server/endpoints/events" "math" "net/http" "net/url" @@ -14,7 +13,7 @@ import ( "strings" "time" - "github.com/PubMatic-OpenWrap/etree" + "github.com/beevik/etree" "github.com/buger/jsonparser" uuid "github.com/gofrs/uuid" "github.com/golang/glog" @@ -23,6 +22,7 @@ import ( accountService "github.com/prebid/prebid-server/account" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/endpoints/events" "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/combination" "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/impressions" @@ -41,12 +41,14 @@ import ( //CTV Specific Endpoint type ctvEndpointDeps struct { endpointDeps - request *openrtb2.BidRequest - reqExt *openrtb_ext.ExtRequestAdPod - impData []*types.ImpData - videoSeats []*openrtb2.SeatBid //stores pure video impression bids - impIndices map[string]int - isAdPodRequest bool + request *openrtb2.BidRequest + reqExt *openrtb_ext.ExtRequestAdPod + impData []*types.ImpData + videoSeats []*openrtb2.SeatBid //stores pure video impression bids + impIndices map[string]int + isAdPodRequest bool + impsExt map[string]map[string]map[string]interface{} + impPartnerBlockedTagIDMap map[string]map[string][]string //Prebid Specific ctx context.Context @@ -382,6 +384,13 @@ func (deps *ctvEndpointDeps) setDefaultValues() { //set request is adpod request or normal request deps.setIsAdPodRequest() + + //TODO: OTT-217, OTT-161 commenting code of filtering vast tags + /* + if deps.isAdPodRequest { + deps.readImpExtensionsAndTags() + } + */ } //validateBidRequest will validate AdPod specific mandatory Parameters and returns error @@ -409,6 +418,42 @@ func (deps *ctvEndpointDeps) validateBidRequest() (err []error) { return } +//readImpExtensionsAndTags will read the impression extensions +func (deps *ctvEndpointDeps) readImpExtensionsAndTags() (errs []error) { + deps.impsExt = make(map[string]map[string]map[string]interface{}) + deps.impPartnerBlockedTagIDMap = make(map[string]map[string][]string) //Initially this will have all tags, eligible tags will be filtered in filterImpsVastTagsByDuration + + for _, imp := range deps.request.Imp { + var impExt map[string]map[string]interface{} + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + errs = append(errs, err) + continue + } + + deps.impPartnerBlockedTagIDMap[imp.ID] = make(map[string][]string) + + for partnerName, partnerExt := range impExt { + impVastTags, ok := partnerExt["tags"].([]interface{}) + if !ok { + continue + } + + for _, tag := range impVastTags { + vastTag, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + deps.impPartnerBlockedTagIDMap[imp.ID][partnerName] = append(deps.impPartnerBlockedTagIDMap[imp.ID][partnerName], vastTag["tagid"].(string)) + } + } + + deps.impsExt[imp.ID] = impExt + } + + return errs +} + /********************* Creating CTV BidRequest *********************/ //createBidRequest will return new bid request with all things copy from bid request except impression objects @@ -421,16 +466,106 @@ func (deps *ctvEndpointDeps) createBidRequest(req *openrtb2.BidRequest) *openrtb //createImpressions ctvRequest.Imp = deps.createImpressions() + //TODO: OTT-217, OTT-161 commenting code of filtering vast tags + //deps.filterImpsVastTagsByDuration(&ctvRequest) + //TODO: remove adpod extension if not required to send further return &ctvRequest } +//filterImpsVastTagsByDuration checks if a Vast tag should be called for a generated impression based on the duration of tag and impression +func (deps *ctvEndpointDeps) filterImpsVastTagsByDuration(bidReq *openrtb2.BidRequest) { + + for impCount, imp := range bidReq.Imp { + index := strings.LastIndex(imp.ID, "_") + if index == -1 { + continue + } + + originalImpID := imp.ID[:index] + + impExtMap := deps.impsExt[originalImpID] + newImpExtMap := make(map[string]map[string]interface{}) + for k, v := range impExtMap { + newImpExtMap[k] = v + } + + for partnerName, partnerExt := range newImpExtMap { + if partnerExt["tags"] != nil { + impVastTags, ok := partnerExt["tags"].([]interface{}) + if !ok { + continue + } + + var compatibleVasts []interface{} + for _, tag := range impVastTags { + vastTag, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + tagDuration := int(vastTag["dur"].(float64)) + if int(imp.Video.MinDuration) <= tagDuration && tagDuration <= int(imp.Video.MaxDuration) { + compatibleVasts = append(compatibleVasts, tag) + + deps.impPartnerBlockedTagIDMap[originalImpID][partnerName] = remove(deps.impPartnerBlockedTagIDMap[originalImpID][partnerName], vastTag["tagid"].(string)) + if len(deps.impPartnerBlockedTagIDMap[originalImpID][partnerName]) == 0 { + delete(deps.impPartnerBlockedTagIDMap[originalImpID], partnerName) + } + } + } + + if len(compatibleVasts) < 1 { + delete(newImpExtMap, partnerName) + } else { + newImpExtMap[partnerName] = map[string]interface{}{ + "tags": compatibleVasts, + } + } + + bExt, err := json.Marshal(newImpExtMap) + if err != nil { + continue + } + imp.Ext = bExt + } + } + bidReq.Imp[impCount] = imp + } + + for impID, blockedTags := range deps.impPartnerBlockedTagIDMap { + for _, datum := range deps.impData { + if datum.ImpID == impID { + datum.BlockedVASTTags = blockedTags + break + } + } + } +} + +func remove(slice []string, item string) []string { + index := -1 + for i := range slice { + if slice[i] == item { + index = i + break + } + } + + if index == -1 { + return slice + } + + return append(slice[:index], slice[index+1:]...) +} + //getAllAdPodImpsConfigs will return all impression adpod configurations func (deps *ctvEndpointDeps) getAllAdPodImpsConfigs() { for index, imp := range deps.request.Imp { if nil == imp.Video || nil == deps.impData[index].VideoExt || nil == deps.impData[index].VideoExt.AdPod { continue } + deps.impData[index].ImpID = imp.ID deps.impData[index].Config = deps.getAdPodImpsConfigs(&imp, deps.impData[index].VideoExt.AdPod) if 0 == len(deps.impData[index].Config) { errorCode := new(int) diff --git a/endpoints/openrtb2/ctv_auction_test.go b/endpoints/openrtb2/ctv_auction_test.go index f03312ead9c..51fa4b499f7 100644 --- a/endpoints/openrtb2/ctv_auction_test.go +++ b/endpoints/openrtb2/ctv_auction_test.go @@ -3,11 +3,12 @@ package openrtb2 import ( "encoding/json" "fmt" - "github.com/PubMatic-OpenWrap/etree" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" "net/url" "strings" "testing" + "github.com/beevik/etree" "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" @@ -149,3 +150,230 @@ func TestAdjustBidIDInVideoEventTrackers(t *testing.T) { } } } + +func TestFilterImpsVastTagsByDuration(t *testing.T) { + type inputParams struct { + request *openrtb2.BidRequest + generatedRequest *openrtb2.BidRequest + impData []*types.ImpData + } + + type output struct { + reqs openrtb2.BidRequest + blockedTags []map[string][]string + } + + tt := []struct { + testName string + input inputParams + expectedOutput output + }{ + { + testName: "test_single_impression_single_vast_partner_with_no_excluded_tags", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 35}}, + }, + }, + impData: []*types.ImpData{}, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 35}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{}, + }, + }, + { + testName: "test_single_impression_single_vast_partner_with_excluded_tags", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35"}}, + }, + }, + }, + { + testName: "test_single_impression_multiple_vast_partners_no_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"},{"dur":25,"tagid":"spotx_25"},{"dur":30,"tagid":"spotx_30"}]},"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{}, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]},"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]},"spotx_vast_bidder":{"tags":[{"dur":25,"tagid":"spotx_25"},{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{}, + }, + }, + { + testName: "test_single_impression_multiple_vast_partners_with_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"},{"dur":25,"tagid":"spotx_25"},{"dur":35,"tagid":"spotx_35"}]},"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":40,"tagid":"openx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]},"spotx_vast_bidder":{"tags":[{"dur":25,"tagid":"spotx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35", "openx_40"}, "spotx_vast_bidder": []string{"spotx_35"}}, + }, + }, + }, + { + testName: "test_multi_impression_multi_partner_no_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp2", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"},{"dur":40,"tagid":"spotx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}}, + }, + }, + impData: nil, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: nil, + }, + }, + { + testName: "test_multi_impression_multi_partner_with_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp2", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"},{"dur":40,"tagid":"spotx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + {ImpID: "imp2"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35"}}, + {"spotx_vast_bidder": []string{"spotx_40"}}, + }, + }, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + deps := ctvEndpointDeps{request: tc.input.request, impData: tc.input.impData} + deps.readImpExtensionsAndTags() + + outputBids := tc.input.generatedRequest + deps.filterImpsVastTagsByDuration(outputBids) + + assert.Equal(t, tc.expectedOutput.reqs, *outputBids, "Expected length of impressions array was %d but actual was %d", tc.expectedOutput.reqs, outputBids) + + for i, datum := range deps.impData { + assert.Equal(t, tc.expectedOutput.blockedTags[i], datum.BlockedVASTTags, "Expected and actual impData was different") + } + }) + } +} diff --git a/errortypes/code.go b/errortypes/code.go index 2749b978006..f8206525b27 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -11,6 +11,7 @@ const ( BidderTemporarilyDisabledErrorCode BlacklistedAcctErrorCode AcctRequiredErrorCode + NoBidPriceErrorCode ) // Defines numeric codes for well-known warnings. diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index 1fed2d7da6e..6bac21fcb0d 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -182,3 +182,19 @@ func (err *Warning) Code() int { func (err *Warning) Severity() Severity { return SeverityWarning } + +type NoBidPrice struct { + Message string +} + +func (err *NoBidPrice) Error() string { + return err.Message +} + +func (err *NoBidPrice) Code() int { + return NoBidPriceErrorCode +} + +func (err *NoBidPrice) Severity() Severity { + return SeverityWarning +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index e3924e5b8cc..6ac494448ca 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -99,6 +99,7 @@ import ( "github.com/prebid/prebid-server/adapters/unicorn" "github.com/prebid/prebid-server/adapters/unruly" "github.com/prebid/prebid-server/adapters/valueimpression" + "github.com/prebid/prebid-server/adapters/vastbidder" "github.com/prebid/prebid-server/adapters/verizonmedia" "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" @@ -214,6 +215,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderUnicorn: unicorn.Builder, openrtb_ext.BidderUnruly: unruly.Builder, openrtb_ext.BidderValueImpression: valueimpression.Builder, + openrtb_ext.BidderVASTBidder: vastbidder.Builder, openrtb_ext.BidderVerizonMedia: verizonmedia.Builder, openrtb_ext.BidderVisx: visx.Builder, openrtb_ext.BidderVrtcal: vrtcal.Builder, diff --git a/exchange/bidder.go b/exchange/bidder.go index 262d2d8d3f3..07d222c9602 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -357,6 +357,12 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall { ext.ResponseBody = string(httpInfo.response.Body) ext.Status = httpInfo.response.StatusCode } + + if nil != httpInfo.request.Params { + ext.Params = make(map[string]int) + ext.Params["ImpIndex"] = httpInfo.request.Params.ImpIndex + ext.Params["VASTTagIndex"] = httpInfo.request.Params.VASTTagIndex + } } return ext diff --git a/exchange/events.go b/exchange/events.go index 35929d8e604..9742e50e424 100644 --- a/exchange/events.go +++ b/exchange/events.go @@ -4,9 +4,8 @@ import ( "encoding/json" "time" - "github.com/mxmCherry/openrtb/v15/openrtb2" - jsonpatch "github.com/evanphx/json-patch" + "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/endpoints/events" @@ -63,7 +62,6 @@ func (ev *eventTracking) modifyBidVAST(pbsBid *pbsOrtbBid, bidderName openrtb_ex if pbsBid.bidType != openrtb_ext.BidTypeVideo || len(bid.AdM) == 0 && len(bid.NURL) == 0 { return } - vastXML := makeVAST(bid) bidID := bid.ID if len(pbsBid.generatedBidID) > 0 { diff --git a/exchange/events_test.go b/exchange/events_test.go index a4fe03601b7..217cca5351d 100644 --- a/exchange/events_test.go +++ b/exchange/events_test.go @@ -1,10 +1,11 @@ package exchange import ( - "github.com/prebid/prebid-server/config" "strings" "testing" + "github.com/prebid/prebid-server/config" + "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" diff --git a/exchange/exchange.go b/exchange/exchange.go index 992f756a5ba..92c53cdc08f 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -27,6 +27,7 @@ import ( "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/prebid_cache_client" + "golang.org/x/net/publicsuffix" ) type ContextKey string @@ -202,6 +203,12 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * var bidResponseExt *openrtb_ext.ExtBidResponse if anyBidsReturned { + adapterBids, rejections := applyAdvertiserBlocking(r.BidRequest, adapterBids) + // add advertiser blocking specific errors + for _, message := range rejections { + errs = append(errs, errors.New(message)) + } + var bidCategory map[string]string //If includebrandcategory is present in ext then CE feature is on. if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil { @@ -444,7 +451,11 @@ func (e *exchange) getAllBids( // Structure to record extra tracking data generated during bidding ae := new(seatResponseExtra) ae.ResponseTimeMillis = int(elapsed / time.Millisecond) + if bids != nil { + // Setting bidderCoreName in SeatBid + bids.bidderCoreName = bidderRequest.BidderCoreName + ae.HttpCalls = bids.httpCalls // Setting bidderCoreName in SeatBid @@ -1043,3 +1054,78 @@ func recordAdaptorDuplicateBidIDs(metricsEngine metrics.MetricsEngine, adapterBi } return bidIDCollisionFound } + +//normalizeDomain validates, normalizes and returns valid domain or error if failed to validate +//checks if domain starts with http by lowercasing entire domain +//if not it prepends it before domain. This is required for obtaining the url +//using url.parse method. on successfull url parsing, it will replace first occurance of www. +//from the domain +func normalizeDomain(domain string) (string, error) { + domain = strings.Trim(strings.ToLower(domain), " ") + // not checking if it belongs to icann + suffix, _ := publicsuffix.PublicSuffix(domain) + if domain != "" && suffix == domain { // input is publicsuffix + return "", errors.New("domain [" + domain + "] is public suffix") + } + if !strings.HasPrefix(domain, "http") { + domain = fmt.Sprintf("http://%s", domain) + } + url, err := url.Parse(domain) + if nil == err && url.Host != "" { + return strings.Replace(url.Host, "www.", "", 1), nil + } + return "", err +} + +//applyAdvertiserBlocking rejects the bids of blocked advertisers mentioned in req.badv +//the rejection is currently only applicable to vast tag bidders. i.e. not for ortb bidders +//it returns seatbids containing valid bids and rejections containing rejected bid.id with reason +func applyAdvertiserBlocking(bidRequest *openrtb2.BidRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string) { + rejections := []string{} + nBadvs := []string{} + if nil != bidRequest.BAdv { + for _, domain := range bidRequest.BAdv { + nDomain, err := normalizeDomain(domain) + if nil == err && nDomain != "" { // skip empty and domains with errors + nBadvs = append(nBadvs, nDomain) + } + } + } + + for bidderName, seatBid := range seatBids { + if seatBid.bidderCoreName == openrtb_ext.BidderVASTBidder && len(nBadvs) > 0 { + for bidIndex := len(seatBid.bids) - 1; bidIndex >= 0; bidIndex-- { + bid := seatBid.bids[bidIndex] + for _, bAdv := range nBadvs { + aDomains := bid.bid.ADomain + rejectBid := false + if nil == aDomains { + // provision to enable rejecting of bids when req.badv is set + rejectBid = true + } else { + for _, d := range aDomains { + if aDomain, err := normalizeDomain(d); nil == err { + // compare and reject bid if + // 1. aDomain == bAdv + // 2. .bAdv is suffix of aDomain + // 3. aDomain not present but request has list of block advertisers + if aDomain == bAdv || strings.HasSuffix(aDomain, "."+bAdv) || (len(aDomain) == 0 && len(bAdv) > 0) { + // aDomain must be subdomain of bAdv + rejectBid = true + break + } + } + } + } + if rejectBid { + // reject the bid. bid belongs to blocked advertisers list + seatBid.bids = append(seatBid.bids[:bidIndex], seatBid.bids[bidIndex+1:]...) + rejections = updateRejections(rejections, bid.bid.ID, fmt.Sprintf("Bid (From '%s') belongs to blocked advertiser '%s'", bidderName, bAdv)) + break // bid is rejected due to advertiser blocked. No need to check further domains + } + } + } + } + } + return seatBids, rejections +} diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 24fa2338e51..58a8788a3f6 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -17,6 +17,7 @@ import ( "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/vastbidder" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/errortypes" @@ -3092,3 +3093,559 @@ type nilCategoryFetcher struct{} func (nilCategoryFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } + +//TestApplyAdvertiserBlocking verifies advertiser blocking +//Currently it is expected to work only with TagBidders and not woth +// normal bidders +func TestApplyAdvertiserBlocking(t *testing.T) { + type args struct { + advBlockReq *openrtb2.BidRequest + adaptorSeatBids map[*bidderAdapter]*pbsOrtbSeatBid // bidder adaptor and its dummy seat bids map + } + type want struct { + rejectedBidIds []string + validBidCountPerSeat map[string]int + } + tests := []struct { + name string + args args + want want + }{ + { + name: "reject_bid_of_blocked_adv_from_tag_bidder", + args: args{ + advBlockReq: &openrtb2.BidRequest{ + BAdv: []string{"a.com"}, // block bids returned by a.com + }, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("vast_tag_bidder"): { // tag bidder returning 1 bid from blocked advertiser + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "a.com_bid", + ADomain: []string{"a.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "b.com_bid", + ADomain: []string{"b.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "keep_ba.com", + ADomain: []string{"ba.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "keep_ba.com", + ADomain: []string{"b.a.com.shri.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "reject_b.a.com.a.com.b.c.d.a.com", + ADomain: []string{"b.a.com.a.com.b.c.d.a.com"}, + }, + }, + }, + bidderCoreName: openrtb_ext.BidderVASTBidder, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"a.com_bid", "reject_b.a.com.a.com.b.c.d.a.com"}, + validBidCountPerSeat: map[string]int{ + "vast_tag_bidder": 3, + }, + }, + }, + { + name: "Badv_is_not_present", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: nil}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tab_bidder_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no bid rejection expected + validBidCountPerSeat: map[string]int{ + "tab_bidder_1": 2, + }, + }, + }, + { + name: "adomain_is_not_present_but_Badv_is_set", // reject bids without adomain as badv is set + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_adapter_1_without_adomain", "bid_2_adapter_1_with_empty_adomain"}, + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 0, // expect 0 bids. i.e. all bids are rejected + "rtb_bidder_1": 2, // no bid must be rejected + }, + }, + }, + { + name: "adomain_and_badv_is_not_present", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adaptor_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_without_adomain"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejection expected as badv not present + validBidCountPerSeat: map[string]int{ + "tag_adaptor_1": 1, + }, + }, + }, + { + name: "empty_badv", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejections expect as there is not badv set + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 2, + "rtb_bidder_1": 2, + }, + }, + }, + { + name: "nil_badv", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: nil}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejections expect as there is not badv set + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 2, + "rtb_bidder_1": 2, + }, + }, + }, + { + name: "ad_domains_normalized_and_checked", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"a.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("my_adapter"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_of_blocked_adv", ADomain: []string{"www.a.com"}}}, + // expect a.com is extracted from page url + {bid: &openrtb2.Bid{ID: "bid_2_of_blocked_adv", ADomain: []string{"http://a.com/my/page?k1=v1&k2=v2"}}}, + // invalid adomain - will be skipped and the bid will be not be rejected + {bid: &openrtb2.Bid{ID: "bid_3_with_domain_abcd1234", ADomain: []string{"abcd1234"}}}, + }, + }}, + }, + want: want{ + rejectedBidIds: []string{"bid_1_of_blocked_adv", "bid_2_of_blocked_adv"}, + validBidCountPerSeat: map[string]int{"my_adapter": 1}, + }, + }, { + name: "multiple_badv", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com", "advertiser_2.com", "www.advertiser_3.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + // adomain without www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_1", ADomain: []string{"advertiser_3.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_tag_adapter_1", ADomain: []string{"advertiser_2.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_3_tag_adapter_1", ADomain: []string{"advertiser_4.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_4_tag_adapter_1", ADomain: []string{"advertiser_100.com"}}}, + }, + }, + newTestTagAdapter("tag_adapter_2"): { + bids: []*pbsOrtbBid{ + // adomain has www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_2", ADomain: []string{"www.advertiser_1.com"}}}, + }, + }, + newTestRtbAdapter("rtb_adapter_1"): { + bids: []*pbsOrtbBid{ + // should not reject following bid though its advertiser is blocked + // because this bid belongs to RTB Adaptor + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_2", ADomain: []string{"advertiser_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_tag_adapter_1", "bid_2_tag_adapter_1", "bid_1_tag_adapter_2"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 2, + "tag_adapter_2": 0, + "rtb_adapter_1": 1, + }, + }, + }, { + name: "multiple_adomain", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"www.advertiser_3.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + // adomain without www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_1", ADomain: []string{"a.com", "b.com", "advertiser_3.com", "d.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_tag_adapter_1", ADomain: []string{"a.com", "https://advertiser_3.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_3_tag_adapter_1", ADomain: []string{"advertiser_4.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_4_tag_adapter_1", ADomain: []string{"advertiser_100.com"}}}, + }, + }, + newTestTagAdapter("tag_adapter_2"): { + bids: []*pbsOrtbBid{ + // adomain has www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_2", ADomain: []string{"a.com", "b.com", "www.advertiser_3.com"}}}, + }, + }, + newTestRtbAdapter("rtb_adapter_1"): { + bids: []*pbsOrtbBid{ + // should not reject following bid though its advertiser is blocked + // because this bid belongs to RTB Adaptor + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_2", ADomain: []string{"a.com", "b.com", "advertiser_3.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_tag_adapter_1", "bid_2_tag_adapter_1", "bid_1_tag_adapter_2"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 2, + "tag_adapter_2": 0, + "rtb_adapter_1": 1, + }, + }, + }, { + name: "case_insensitive_badv", // case of domain not matters + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"ADVERTISER_1.COM"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_1", ADomain: []string{"advertiser_1.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_rtb_adapter_1", ADomain: []string{"www.advertiser_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_rtb_adapter_1", "bid_2_rtb_adapter_1"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 0, // expect all bids are rejected as belongs to blocked advertiser + }, + }, + }, + { + name: "case_insensitive_adomain", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_1", ADomain: []string{"advertiser_1.COM"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_rtb_adapter_1", ADomain: []string{"wWw.ADVERTISER_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_rtb_adapter_1", "bid_2_rtb_adapter_1"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 0, // expect all bids are rejected as belongs to blocked advertiser + }, + }, + }, + { + name: "various_tld_combinations", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"http://blockme.shri"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("block_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"www.blockme.shri"}, ID: "reject_www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://www.blockme.shri"}, ID: "rejecthttp://www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://blockme.shri"}, ID: "reject_https://blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://www.blockme.shri"}, ID: "reject_https://www.blockme.shri"}}, + }, + }, + newTestRtbAdapter("rtb_non_block_bidder"): { + bids: []*pbsOrtbBid{ // all below bids are eligible and should not be rejected + {bid: &openrtb2.Bid{ADomain: []string{"www.blockme.shri"}, ID: "accept_bid_www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://www.blockme.shri"}, ID: "accept_bid__http://www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://blockme.shri"}, ID: "accept_bid__https://blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://www.blockme.shri"}, ID: "accept_bid__https://www.blockme.shri"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"reject_www.blockme.shri", "reject_http://www.blockme.shri", "reject_https://blockme.shri", "reject_https://www.blockme.shri"}, + validBidCountPerSeat: map[string]int{ + "block_bidder": 0, + "rtb_non_block_bidder": 4, + }, + }, + }, + { + name: "subdomain_tests", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"10th.college.puneunv.edu"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("block_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"shri.10th.college.puneunv.edu"}, ID: "reject_shri.10th.college.puneunv.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"puneunv.edu"}, ID: "allow_puneunv.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://WWW.123.456.10th.college.PUNEUNV.edu"}, ID: "reject_123.456.10th.college.puneunv.edu"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"reject_shri.10th.college.puneunv.edu", "reject_123.456.10th.college.puneunv.edu"}, + validBidCountPerSeat: map[string]int{ + "block_bidder": 1, + }, + }, + }, { + name: "only_domain_test", // do not expect bid rejection. edu is valid domain + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"edu"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"school.edu"}, ID: "keep_bid_school.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"edu"}, ID: "keep_bid_edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"..edu"}, ID: "keep_bid_..edu"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, + validBidCountPerSeat: map[string]int{ + "tag_bidder": 3, + }, + }, + }, + { + name: "public_suffix_in_badv", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"co.in"}}, // co.in is valid public suffix + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"a.co.in"}, ID: "allow_a.co.in"}}, + {bid: &openrtb2.Bid{ADomain: []string{"b.com"}, ID: "allow_b.com"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, + validBidCountPerSeat: map[string]int{ + "tag_bidder": 2, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name != "reject_bid_of_blocked_adv_from_tag_bidder" { + return + } + seatBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + tagBidders := make(map[openrtb_ext.BidderName]adapters.Bidder) + adapterMap := make(map[openrtb_ext.BidderName]adaptedBidder, 0) + for adaptor, sbids := range tt.args.adaptorSeatBids { + adapterMap[adaptor.BidderName] = adaptor + if tagBidder, ok := adaptor.Bidder.(*vastbidder.TagBidder); ok { + tagBidders[adaptor.BidderName] = tagBidder + } + seatBids[adaptor.BidderName] = sbids + } + + // applyAdvertiserBlocking internally uses tagBidders from (adapter_map.go) + // not testing alias here + seatBids, rejections := applyAdvertiserBlocking(tt.args.advBlockReq, seatBids) + + re := regexp.MustCompile("bid rejected \\[bid ID:(.*?)\\] reason") + for bidder, sBid := range seatBids { + // verify only eligible bids are returned + assert.Equal(t, tt.want.validBidCountPerSeat[string(bidder)], len(sBid.bids), "Expected eligible bids are %d, but found [%d] ", tt.want.validBidCountPerSeat[string(bidder)], len(sBid.bids)) + // verify rejections + assert.Equal(t, len(tt.want.rejectedBidIds), len(rejections), "Expected bid rejections are %d, but found [%d]", len(tt.want.rejectedBidIds), len(rejections)) + // verify rejected bid ids + present := false + for _, expectRejectedBidID := range tt.want.rejectedBidIds { + for _, rejection := range rejections { + match := re.FindStringSubmatch(rejection) + rejectedBidID := strings.Trim(match[1], " ") + if expectRejectedBidID == rejectedBidID { + present = true + break + } + } + if present { + break + } + } + if len(tt.want.rejectedBidIds) > 0 && !present { + assert.Fail(t, "Expected Bid ID [%s] as rejected. But bid is not rejected", re) + } + + if sBid.bidderCoreName != openrtb_ext.BidderVASTBidder { + continue // advertiser blocking is currently enabled only for tag bidders + } + // verify eligible bids not belongs to blocked advertisers + for _, bid := range sBid.bids { + if nil != bid.bid.ADomain { + for _, adomain := range bid.bid.ADomain { + for _, blockDomain := range tt.args.advBlockReq.BAdv { + nDomain, _ := normalizeDomain(adomain) + if nDomain == blockDomain { + assert.Fail(t, "bid %s with ad domain %s is not blocked", bid.bid.ID, adomain) + } + } + } + } + + // verify this bid not belongs to rejected list + for _, rejectedBidID := range tt.want.rejectedBidIds { + if rejectedBidID == bid.bid.ID { + assert.Fail(t, "Bid ID [%s] is not expected in list of rejected bids", bid.bid.ID) + } + } + } + } + }) + } +} + +func TestNormalizeDomain(t *testing.T) { + type args struct { + domain string + } + type want struct { + domain string + err error + } + tests := []struct { + name string + args args + want want + }{ + {name: "a.com", args: args{domain: "a.com"}, want: want{domain: "a.com"}}, + {name: "http://a.com", args: args{domain: "http://a.com"}, want: want{domain: "a.com"}}, + {name: "https://a.com", args: args{domain: "https://a.com"}, want: want{domain: "a.com"}}, + {name: "https://www.a.com", args: args{domain: "https://www.a.com"}, want: want{domain: "a.com"}}, + {name: "https://www.a.com/my/page?k=1", args: args{domain: "https://www.a.com/my/page?k=1"}, want: want{domain: "a.com"}}, + {name: "empty_domain", args: args{domain: ""}, want: want{domain: ""}}, + {name: "trim_domain", args: args{domain: " trim.me?k=v "}, want: want{domain: "trim.me"}}, + {name: "trim_domain_with_http_in_it", args: args{domain: " http://trim.me?k=v "}, want: want{domain: "trim.me"}}, + {name: "https://www.something.a.com/my/page?k=1", args: args{domain: "https://www.something.a.com/my/page?k=1"}, want: want{domain: "something.a.com"}}, + {name: "wWW.something.a.com", args: args{domain: "wWW.something.a.com"}, want: want{domain: "something.a.com"}}, + {name: "2_times_www", args: args{domain: "www.something.www.a.com"}, want: want{domain: "something.www.a.com"}}, + {name: "consecutive_www", args: args{domain: "www.www.something.a.com"}, want: want{domain: "www.something.a.com"}}, + {name: "abchttp.com", args: args{domain: "abchttp.com"}, want: want{domain: "abchttp.com"}}, + {name: "HTTP://CAPS.com", args: args{domain: "HTTP://CAPS.com"}, want: want{domain: "caps.com"}}, + + // publicsuffix + {name: "co.in", args: args{domain: "co.in"}, want: want{domain: "", err: fmt.Errorf("domain [co.in] is public suffix")}}, + {name: ".co.in", args: args{domain: ".co.in"}, want: want{domain: ".co.in"}}, + {name: "amazon.co.in", args: args{domain: "amazon.co.in"}, want: want{domain: "amazon.co.in"}}, + // we wont check if shriprasad belongs to icann + {name: "shriprasad", args: args{domain: "shriprasad"}, want: want{domain: "", err: fmt.Errorf("domain [shriprasad] is public suffix")}}, + {name: ".shriprasad", args: args{domain: ".shriprasad"}, want: want{domain: ".shriprasad"}}, + {name: "abc.shriprasad", args: args{domain: "abc.shriprasad"}, want: want{domain: "abc.shriprasad"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adjustedDomain, err := normalizeDomain(tt.args.domain) + actualErr := "nil" + expectedErr := "nil" + if nil != err { + actualErr = err.Error() + } + if nil != tt.want.err { + actualErr = tt.want.err.Error() + } + assert.Equal(t, tt.want.err, err, "Expected error is %s, but found [%s]", expectedErr, actualErr) + assert.Equal(t, tt.want.domain, adjustedDomain, "Expected domain is %s, but found [%s]", tt.want.domain, adjustedDomain) + }) + } +} + +func newTestTagAdapter(name string) *bidderAdapter { + return &bidderAdapter{ + Bidder: vastbidder.NewTagBidder(openrtb_ext.BidderName(name), config.Adapter{}), + BidderName: openrtb_ext.BidderName(name), + } +} + +func newTestRtbAdapter(name string) *bidderAdapter { + return &bidderAdapter{ + Bidder: &goodSingleBidder{}, + BidderName: openrtb_ext.BidderName(name), + } +} diff --git a/go.mod b/go.mod index 13cd3748779..dee3615b79b 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/DATA-DOG/go-sqlmock v1.3.0 github.com/NYTimes/gziphandler v1.1.1 github.com/OneOfOne/xxhash v1.2.5 // indirect - github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4 github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf + github.com/beevik/etree v1.0.2 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/blang/semver v3.5.1+incompatible github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 @@ -60,3 +60,5 @@ require ( ) replace github.com/mxmCherry/openrtb/v15 v15.0.0 => github.com/PubMatic-OpenWrap/openrtb/v15 v15.0.0-20210425063110-b01110089669 + +replace github.com/beevik/etree v1.0.2 => github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4 diff --git a/go.sum b/go.sum index ce383174fb8..0ccf122d248 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/PubMatic-OpenWrap/openrtb/v15 v15.0.0-20210425134848-adbedb6f42c6 h1: github.com/PubMatic-OpenWrap/openrtb/v15 v15.0.0-20210425134848-adbedb6f42c6/go.mod h1:XMFHmDvfVyIhz5JGzvNXVt0afDLN2mrdYviUOKIYqAo= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index a568392beba..f3cfb3df6a4 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -58,6 +58,7 @@ type ExtBidPrebidMeta struct { type ExtBidPrebidVideo struct { Duration int `json:"duration"` PrimaryCategory string `json:"primary_category"` + VASTTagID string `json:"vasttagid"` } // ExtBidPrebidEvents defines the contract for bidresponse.seatbid.bid[i].ext.prebid.events diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index ef114914cd6..7d5684600bb 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -173,6 +173,7 @@ const ( BidderUnicorn BidderName = "unicorn" BidderUnruly BidderName = "unruly" BidderValueImpression BidderName = "valueimpression" + BidderVASTBidder BidderName = "vastbidder" BidderVerizonMedia BidderName = "verizonmedia" BidderVisx BidderName = "visx" BidderVrtcal BidderName = "vrtcal" @@ -286,6 +287,7 @@ func CoreBidderNames() []BidderName { BidderUnicorn, BidderUnruly, BidderValueImpression, + BidderVASTBidder, BidderVerizonMedia, BidderVisx, BidderVrtcal, diff --git a/openrtb_ext/imp_vastbidder.go b/openrtb_ext/imp_vastbidder.go new file mode 100644 index 00000000000..2923c2dd8d7 --- /dev/null +++ b/openrtb_ext/imp_vastbidder.go @@ -0,0 +1,18 @@ +package openrtb_ext + +// ExtImpVASTBidder defines the contract for bidrequest.imp[i].ext.vastbidder +type ExtImpVASTBidder struct { + Tags []*ExtImpVASTBidderTag `json:"tags,omitempty"` + Parser string `json:"parser,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Cookies map[string]string `json:"cookies,omitempty"` +} + +// ExtImpVASTBidderTag defines the contract for bidrequest.imp[i].ext.pubmatic.tags[i] +type ExtImpVASTBidderTag struct { + TagID string `json:"tagid"` + URL string `json:"url"` + Duration int `json:"dur"` + Price float64 `json:"price"` + Params map[string]interface{} `json:"params,omitempty"` +} diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index 1c7177daf49..a1d1e18d380 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -59,6 +59,7 @@ type ExtHttpCall struct { RequestHeaders map[string][]string `json:"requestheaders"` ResponseBody string `json:"responsebody"` Status int `json:"status"` + Params map[string]int `json:"params,omitempty"` } // CookieStatus describes the allowed values for bidresponse.ext.usersync.{bidder}.status diff --git a/static/bidder-info/vastbidder.yaml b/static/bidder-info/vastbidder.yaml new file mode 100644 index 00000000000..b8eb41d4e49 --- /dev/null +++ b/static/bidder-info/vastbidder.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "UOEDev@pubmatic.com" +capabilities: + app: + mediaTypes: + - video + site: + mediaTypes: + - video diff --git a/static/bidder-params/vastbidder.json b/static/bidder-params/vastbidder.json new file mode 100644 index 00000000000..0bef9b5fd5e --- /dev/null +++ b/static/bidder-params/vastbidder.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Tag Bidder Base Adapter", + "description": "A schema which validates params accepted by the VAST tag bidders", + + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tagid": { "type": "string" }, + "url": { "type": "string" }, + "dur": { "type": "integer" }, + "price": { "type": "number" }, + "params": { "type": "object" } + }, + "required": [ "tagid", "url", "dur" ] + } + }, + "parser": { "type": "string" }, + "headers": { "type": "object" }, + "cookies": { "type": "object" } + }, + "required": ["tags"] +} \ No newline at end of file diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 7e10c41cd76..10a95fb4b67 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -124,6 +124,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderSilverMob: true, openrtb_ext.BidderSmaato: true, openrtb_ext.BidderSpotX: true, + openrtb_ext.BidderVASTBidder: true, openrtb_ext.BidderUnicorn: true, openrtb_ext.BidderYeahmobi: true, }