@@ -6,7 +6,7 @@ use adex_primitives::{
66 } ,
77 targeting:: { self , input} ,
88 util:: ApiUrl ,
9- Address , BigNum , CampaignId , ToHex , UnifiedNum , IPFS ,
9+ Address , BigNum , CampaignId , UnifiedNum , IPFS ,
1010} ;
1111use async_std:: { sync:: RwLock , task:: block_on} ;
1212use chrono:: { DateTime , Duration , Utc } ;
@@ -49,10 +49,14 @@ pub static DEFAULT_TOKENS: Lazy<HashSet<Address>> = Lazy::new(|| {
4949
5050#[ derive( Debug , Error ) ]
5151pub enum Error {
52- #[ error( "Request to the Market failed: status {status} at url {url}" ) ]
53- Market { status : StatusCode , url : Url } ,
52+ #[ error( "Request to the Sentry failed: status {status} at url {url}" ) ]
53+ Sentry { status : StatusCode , url : Url } ,
5454 #[ error( transparent) ]
5555 Request ( #[ from] reqwest:: Error ) ,
56+ #[ error( "No validators provided" ) ]
57+ NoValidators ,
58+ #[ error( "Invalid validator URL" ) ]
59+ InvalidValidatorUrl ,
5660}
5761
5862/// The Ad [`Manager`]'s options for showing ads.
@@ -77,6 +81,8 @@ pub struct Options {
7781 /// default: `false`
7882 #[ serde( default ) ]
7983 pub disabled_sticky : bool ,
84+ /// List of validators to query /units-for-slot from
85+ pub validators : Vec < ApiUrl > ,
8086}
8187
8288/// [`AdSlot`](adex_primitives::AdSlot) size `width x height` in pixels (`px`)
@@ -240,40 +246,53 @@ impl Manager {
240246 }
241247 }
242248
243- pub async fn get_market_demand_resp ( & self ) -> Result < Response , Error > {
244- let pub_prefix = self . options . publisher_addr . to_hex ( ) ;
245-
246- let deposit_asset = self
249+ // Test with different units with price
250+ // Test if first campaign is not overwritten
251+ pub async fn get_units_for_slot_resp ( & self ) -> Result < Response , Error > {
252+ let deposit_assets = self
247253 . options
248254 . whitelisted_tokens
249255 . iter ( )
250- . map ( |token| format ! ( "depositAsset ={}" , token) )
256+ . map ( |token| format ! ( "depositAssets[] ={}" , token) )
251257 . collect :: < Vec < _ > > ( )
252258 . join ( "&" ) ;
253259
254- // ApiUrl handles endpoint path (with or without `/`)
255- let url = self
256- . options
257- . market_url
260+ let first_validator = self . options . validators . get ( 0 ) . ok_or ( Error :: NoValidators ) ?;
261+
262+ let url = first_validator
258263 . join ( & format ! (
259- "units-for-slot/{ad_slot}?pubPrefix={pub_prefix}&{deposit_asset }" ,
260- ad_slot = self . options. market_slot
264+ "v5/ units-for-slot/{}?{ }" ,
265+ self . options. market_slot, deposit_assets
261266 ) )
262- . expect ( "Valid URL endpoint!" ) ;
263-
264- let market_response = self . client . get ( url. clone ( ) ) . send ( ) . await ?;
265-
266- match market_response. status ( ) {
267- StatusCode :: OK => Ok ( market_response. json ( ) . await ?) ,
268- _ => Err ( Error :: Market {
269- status : market_response. status ( ) ,
270- url,
271- } ) ,
267+ . map_err ( |_| Error :: InvalidValidatorUrl ) ?;
268+ // Ordering of the campaigns matters so we will just push them to the first result
269+ // We reuse `targeting_input_base`, `accepted_referrers` and `fallback_unit`
270+ let mut first_res: Response = self . client . get ( url. as_str ( ) ) . send ( ) . await ?. json ( ) . await ?;
271+
272+ for validator in self . options . validators . iter ( ) . skip ( 1 ) {
273+ let url = validator
274+ . join ( & format ! (
275+ "v5/units-for-slot/{}?{}" ,
276+ self . options. market_slot, deposit_assets
277+ ) )
278+ . map_err ( |_| Error :: InvalidValidatorUrl ) ?;
279+ let new_res: Response = self . client . get ( url. as_str ( ) ) . send ( ) . await ?. json ( ) . await ?;
280+ for response_campaign in new_res. campaigns {
281+ if !first_res
282+ . campaigns
283+ . iter ( )
284+ . any ( |c| c. campaign . id == response_campaign. campaign . id )
285+ {
286+ first_res. campaigns . push ( response_campaign) ;
287+ }
288+ }
272289 }
290+
291+ Ok ( first_res)
273292 }
274293
275294 pub async fn get_next_ad_unit ( & self ) -> Result < Option < NextAdUnit > , Error > {
276- let units_for_slot = self . get_market_demand_resp ( ) . await ?;
295+ let units_for_slot = self . get_units_for_slot_resp ( ) . await ?;
277296 let m_campaigns = & units_for_slot. campaigns ;
278297 let fallback_unit = units_for_slot. fallback_unit ;
279298 let targeting_input = units_for_slot. targeting_input_base ;
@@ -418,3 +437,174 @@ impl Manager {
418437 }
419438 }
420439}
440+
441+ #[ cfg( test) ]
442+ mod test {
443+ use super :: * ;
444+ use crate :: manager:: input:: Input ;
445+ use adex_primitives:: {
446+ sentry:: CLICK ,
447+ test_util:: { CAMPAIGNS , DUMMY_AD_UNITS , DUMMY_IPFS , PUBLISHER } ,
448+ } ;
449+ use wiremock:: {
450+ matchers:: { method, path} ,
451+ Mock , MockServer , ResponseTemplate ,
452+ } ;
453+
454+ #[ tokio:: test]
455+ async fn test_querying_for_units_for_slot ( ) {
456+ // 1. Set up mock servers for each validator
457+ let server = MockServer :: start ( ) . await ;
458+ let slot = DUMMY_IPFS [ 0 ] ;
459+ let seconds_since_epoch = Utc :: now ( ) ;
460+
461+ let original_input = Input {
462+ ad_view : None ,
463+ global : input:: Global {
464+ ad_slot_id : DUMMY_IPFS [ 0 ] ,
465+ ad_slot_type : "legacy_250x250" . to_string ( ) ,
466+ publisher_id : * PUBLISHER ,
467+ country : None ,
468+ event_type : IMPRESSION ,
469+ // we can't know only the timestamp
470+ seconds_since_epoch,
471+ user_agent_os : Some ( "Linux" . to_string ( ) ) ,
472+ user_agent_browser_family : Some ( "Firefox" . to_string ( ) ) ,
473+ } ,
474+ // no AdUnit should be present
475+ ad_unit_id : None ,
476+ // no balances
477+ balances : None ,
478+ // no campaign
479+ campaign : None ,
480+ ad_slot : Some ( input:: AdSlot {
481+ categories : vec ! [ "IAB3" . into( ) , "IAB13-7" . into( ) , "IAB5" . into( ) ] ,
482+ hostname : "adex.network" . to_string ( ) ,
483+ } ) ,
484+ } ;
485+
486+ let modified_input = Input {
487+ ad_view : None ,
488+ global : input:: Global {
489+ ad_slot_id : DUMMY_IPFS [ 1 ] ,
490+ ad_slot_type : "legacy_250x250" . to_string ( ) ,
491+ publisher_id : * PUBLISHER ,
492+ country : None ,
493+ event_type : CLICK ,
494+ // we can't know only the timestamp
495+ seconds_since_epoch,
496+ user_agent_os : Some ( "Linux" . to_string ( ) ) ,
497+ user_agent_browser_family : Some ( "Firefox" . to_string ( ) ) ,
498+ } ,
499+ // no AdUnit should be present
500+ ad_unit_id : None ,
501+ // no balances
502+ balances : None ,
503+ // no campaign
504+ campaign : None ,
505+ ad_slot : Some ( input:: AdSlot {
506+ categories : vec ! [ "IAB3" . into( ) , "IAB13-7" . into( ) , "IAB5" . into( ) ] ,
507+ hostname : "adex.network" . to_string ( ) ,
508+ } ) ,
509+ } ;
510+
511+ let original_referrers = vec ! [ Url :: parse( "https://ambire.com" ) . expect( "should parse" ) ] ;
512+ let modified_referrers =
513+ vec ! [ Url :: parse( "https://www.google.com/adsense/start/" ) . expect( "should parse" ) ] ;
514+
515+ let original_ad_unit = AdUnit :: from ( & DUMMY_AD_UNITS [ 0 ] ) ;
516+ let modified_ad_unit = AdUnit :: from ( & DUMMY_AD_UNITS [ 1 ] ) ;
517+
518+ let campaign_0 = Campaign {
519+ campaign : CAMPAIGNS [ 0 ] . context . clone ( ) ,
520+ units_with_price : Vec :: new ( ) ,
521+ } ;
522+
523+ let campaign_1 = Campaign {
524+ campaign : CAMPAIGNS [ 1 ] . context . clone ( ) ,
525+ units_with_price : Vec :: new ( ) ,
526+ } ;
527+
528+ let campaign_2 = Campaign {
529+ campaign : CAMPAIGNS [ 2 ] . context . clone ( ) ,
530+ units_with_price : Vec :: new ( ) ,
531+ } ;
532+
533+ // Original response
534+ let response_1 = Response {
535+ targeting_input_base : original_input. clone ( ) ,
536+ accepted_referrers : original_referrers. clone ( ) ,
537+ fallback_unit : Some ( original_ad_unit. clone ( ) ) ,
538+ campaigns : vec ! [ campaign_0. clone( ) ] ,
539+ } ;
540+
541+ // Different targeting_input_base, fallback_unit, accepted_referrers, 1 new campaign and 1 repeating campaign
542+ let response_2 = Response {
543+ targeting_input_base : modified_input. clone ( ) ,
544+ accepted_referrers : modified_referrers. clone ( ) ,
545+ fallback_unit : Some ( modified_ad_unit. clone ( ) ) ,
546+ campaigns : vec ! [ campaign_0. clone( ) , campaign_1. clone( ) ] ,
547+ } ;
548+
549+ // 1 new campaigns, 2 repeating campaigns
550+ let response_3 = Response {
551+ targeting_input_base : modified_input,
552+ accepted_referrers : modified_referrers,
553+ fallback_unit : Some ( modified_ad_unit) ,
554+ campaigns : vec ! [ campaign_0. clone( ) , campaign_1. clone( ) , campaign_2. clone( ) ] ,
555+ } ;
556+
557+ Mock :: given ( method ( "GET" ) )
558+ . and ( path ( format ! ( "validator-1/v5/units-for-slot/{}" , slot) ) )
559+ . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( & response_1) )
560+ . mount ( & server)
561+ . await ;
562+
563+ Mock :: given ( method ( "GET" ) )
564+ . and ( path ( format ! ( "validator-2/v5/units-for-slot/{}" , slot) ) )
565+ . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( & response_2) )
566+ . mount ( & server)
567+ . await ;
568+
569+ Mock :: given ( method ( "GET" ) )
570+ . and ( path ( format ! ( "validator-3/v5/units-for-slot/{}" , slot, ) ) )
571+ . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( & response_3) )
572+ . mount ( & server)
573+ . await ;
574+
575+ // 2. Set up a manager
576+ let market_url = server. uri ( ) . parse ( ) . unwrap ( ) ;
577+ let whitelisted_tokens = DEFAULT_TOKENS . clone ( ) ;
578+
579+ let validator_1_url =
580+ ApiUrl :: parse ( & format ! ( "{}/validator-1" , server. uri( ) ) ) . expect ( "should parse" ) ;
581+ let validator_2_url =
582+ ApiUrl :: parse ( & format ! ( "{}/validator-2" , server. uri( ) ) ) . expect ( "should parse" ) ;
583+ let validator_3_url =
584+ ApiUrl :: parse ( & format ! ( "{}/validator-3" , server. uri( ) ) ) . expect ( "should parse" ) ;
585+ let options = Options {
586+ market_url,
587+ market_slot : DUMMY_IPFS [ 0 ] ,
588+ publisher_addr : * PUBLISHER ,
589+ // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared
590+ whitelisted_tokens,
591+ size : Some ( Size :: new ( 300 , 100 ) ) ,
592+ navigator_language : Some ( "bg" . into ( ) ) ,
593+ disabled_video : false ,
594+ disabled_sticky : false ,
595+ validators : vec ! [ validator_1_url, validator_2_url, validator_3_url] ,
596+ } ;
597+
598+ let manager = Manager :: new ( options. clone ( ) , Default :: default ( ) )
599+ . expect ( "Failed to create AdView Manager" ) ;
600+
601+ let res = manager
602+ . get_units_for_slot_resp ( )
603+ . await
604+ . expect ( "Should get response" ) ;
605+ assert_eq ! ( res. targeting_input_base. global. ad_slot_id, DUMMY_IPFS [ 0 ] ) ;
606+ assert_eq ! ( res. accepted_referrers, original_referrers) ;
607+ assert_eq ! ( res. fallback_unit, Some ( original_ad_unit) ) ;
608+ assert_eq ! ( res. campaigns, vec![ campaign_0, campaign_1, campaign_2] ) ;
609+ }
610+ }
0 commit comments