@@ -6,7 +6,7 @@ use adex_primitives::{
6
6
} ,
7
7
targeting:: { self , input} ,
8
8
util:: ApiUrl ,
9
- Address , BigNum , CampaignId , ToHex , UnifiedNum , IPFS ,
9
+ Address , BigNum , CampaignId , UnifiedNum , IPFS ,
10
10
} ;
11
11
use async_std:: { sync:: RwLock , task:: block_on} ;
12
12
use chrono:: { DateTime , Duration , Utc } ;
@@ -49,10 +49,14 @@ pub static DEFAULT_TOKENS: Lazy<HashSet<Address>> = Lazy::new(|| {
49
49
50
50
#[ derive( Debug , Error ) ]
51
51
pub 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 } ,
54
54
#[ error( transparent) ]
55
55
Request ( #[ from] reqwest:: Error ) ,
56
+ #[ error( "No validators provided" ) ]
57
+ NoValidators ,
58
+ #[ error( "Invalid validator URL" ) ]
59
+ InvalidValidatorUrl ,
56
60
}
57
61
58
62
/// The Ad [`Manager`]'s options for showing ads.
@@ -77,6 +81,8 @@ pub struct Options {
77
81
/// default: `false`
78
82
#[ serde( default ) ]
79
83
pub disabled_sticky : bool ,
84
+ /// List of validators to query /units-for-slot from
85
+ pub validators : Vec < ApiUrl > ,
80
86
}
81
87
82
88
/// [`AdSlot`](adex_primitives::AdSlot) size `width x height` in pixels (`px`)
@@ -240,40 +246,53 @@ impl Manager {
240
246
}
241
247
}
242
248
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
247
253
. options
248
254
. whitelisted_tokens
249
255
. iter ( )
250
- . map ( |token| format ! ( "depositAsset ={}" , token) )
256
+ . map ( |token| format ! ( "depositAssets[] ={}" , token) )
251
257
. collect :: < Vec < _ > > ( )
252
258
. join ( "&" ) ;
253
259
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
258
263
. 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
261
266
) )
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
+ }
272
289
}
290
+
291
+ Ok ( first_res)
273
292
}
274
293
275
294
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 ?;
277
296
let m_campaigns = & units_for_slot. campaigns ;
278
297
let fallback_unit = units_for_slot. fallback_unit ;
279
298
let targeting_input = units_for_slot. targeting_input_base ;
@@ -418,3 +437,174 @@ impl Manager {
418
437
}
419
438
}
420
439
}
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