Skip to content

Commit 1d27a27

Browse files
authored
Merge pull request #562 from AmbireTech/adview-manager-units-for-slot
Adview manager units for slot
2 parents 62eb194 + cd5cddd commit 1d27a27

File tree

5 files changed

+236
-35
lines changed

5 files changed

+236
-35
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

adview-manager/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ rand = "0.8"
3131
[dev-dependencies]
3232
# enable the `test-util` only in dev
3333
adex_primitives = { version = "0.2", path = "../primitives", package = "primitives", features = ["test-util"] }
34+
wiremock = "0.5"
35+
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

adview-manager/serve/src/routes.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use std::sync::Arc;
33
use adex_primitives::{
44
sentry::{units_for_slot, IMPRESSION},
55
targeting::{input::Global, Input},
6-
test_util::{DUMMY_CAMPAIGN, DUMMY_IPFS},
6+
test_util::{DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, DUMMY_VALIDATOR_LEADER},
7+
util::ApiUrl,
78
ToHex,
89
};
910
use adview_manager::{
@@ -33,7 +34,7 @@ pub async fn get_index(Extension(state): Extension<Arc<State>>) -> Html<String>
3334

3435
/// `GET /preview/ad`
3536
pub async fn get_preview_ad(Extension(state): Extension<Arc<State>>) -> Html<String> {
36-
// For mocking the `get_market_demand_resp` call
37+
// For mocking the `get_units_for_slot_resp` call
3738
let mock_server = MockServer::start().await;
3839

3940
let market_url = mock_server.uri().parse().unwrap();
@@ -55,6 +56,10 @@ pub async fn get_preview_ad(Extension(state): Extension<Arc<State>>) -> Html<Str
5556
/// Defaulted
5657
disabled_video,
5758
disabled_sticky: false,
59+
validators: vec![
60+
ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"),
61+
ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"),
62+
],
5863
};
5964

6065
let manager =
@@ -84,26 +89,24 @@ pub async fn get_preview_ad(Extension(state): Extension<Arc<State>>) -> Html<Str
8489
campaigns: vec![],
8590
};
8691

87-
// Mock the `get_market_demand_resp` call
92+
// Mock the `get_units_for_slot_resp` call
8893
let mock_call = Mock::given(method("GET"))
89-
// &depositAsset={}&depositAsset={}
9094
.and(path(format!("units-for-slot/{}", options.market_slot)))
91-
// pubPrefix=HEX&depositAsset=0xASSET1&depositAsset=0xASSET2
95+
// pubPrefix=HEX&depositAssets[]=0xASSET1&depositAssets[]=0xASSET2
9296
.and(query_param("pubPrefix", pub_prefix))
9397
.and(query_param(
94-
"depositAsset",
98+
"depositAssets[]",
9599
"0x6B175474E89094C44Da98b954EedeAC495271d0F",
96100
))
97-
// .and(query_param("depositAsset[]", "0x6B175474E89094C44Da98b954EedeAC495271d03"))
98101
.respond_with(ResponseTemplate::new(200).set_body_json(units_for_slot_resp))
99102
.expect(1)
100-
.named("get_market_demand_resp");
103+
.named("get_units_for_slot_resp");
101104

102105
// Mounting the mock on the mock server - it's now effective!
103106
mock_call.mount(&mock_server).await;
104107

105108
let demand_resp = manager
106-
.get_market_demand_resp()
109+
.get_units_for_slot_resp()
107110
.await
108111
.expect("Should return Mocked response");
109112

@@ -159,6 +162,10 @@ pub async fn get_preview_video(Extension(state): Extension<Arc<State>>) -> Html<
159162
/// Defaulted
160163
disabled_video,
161164
disabled_sticky: false,
165+
validators: vec![
166+
ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"),
167+
ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"),
168+
],
162169
};
163170

164171
// legacy_728x90

adview-manager/src/manager.rs

Lines changed: 215 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};
1111
use async_std::{sync::RwLock, task::block_on};
1212
use chrono::{DateTime, Duration, Utc};
@@ -49,10 +49,14 @@ pub static DEFAULT_TOKENS: Lazy<HashSet<Address>> = Lazy::new(|| {
4949

5050
#[derive(Debug, Error)]
5151
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 },
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+
}

sentry/src/routes/units_for_slot_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const TEST_AD_SLOT: Lazy<(AdSlot, AdUnit)> = Lazy::new(|| {
8484
});
8585

8686
async fn setup_mocked_platform_dummy_app() -> (MockServer, ApplicationGuard) {
87-
// For mocking the `get_market_demand_resp` call
87+
// For mocking the `get_units_for_slot_resp` call
8888
let mock_server = MockServer::start().await;
8989

9090
let platform_url = mock_server.uri().parse().unwrap();

0 commit comments

Comments
 (0)