diff --git a/Cargo.lock b/Cargo.lock index 896b610..a926352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,7 +1563,7 @@ dependencies = [ [[package]] name = "primitives" version = "0.1.0" -source = "git+https://github.com/AdExNetwork/adex-validator-stack-rust?branch=dev#4015d8187f0d76f974a06c16492b17e94b18f9ce" +source = "git+https://github.com/AdExNetwork/adex-validator-stack-rust?branch=bugfixes#20ac5e78b6c2bc94aebe4806a256421247a3866b" dependencies = [ "async-trait", "chrono", @@ -2126,6 +2126,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.2" @@ -2134,9 +2143,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "slog" -version = "2.5.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" [[package]] name = "slog-async" @@ -2152,9 +2161,9 @@ dependencies = [ [[package]] name = "slog-term" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "124501187c410b6a46fe8a47a48435ae462fae4e02d03c558d358f40b17308cb" +checksum = "bab1d807cf71129b05ce36914e1dbb6fbfbdecaf686301cb457f4fa967f9f5b6" dependencies = [ "atty", "chrono", @@ -2206,6 +2215,7 @@ dependencies = [ "futures 0.3.5", "http", "hyper", + "hyper-tls", "lazy_static", "pretty_assertions", "primitives", @@ -2344,12 +2354,16 @@ dependencies = [ "futures-core", "iovec", "lazy_static", + "libc", "memchr", "mio", + "mio-uds", "num_cpus", "pin-project-lite", + "signal-hook-registry", "slab", "tokio-macros", + "winapi 0.3.8", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2a8a83d..86a38d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,15 @@ authors = ["Lachezar Lechev "] edition = "2018" [dependencies] -primitives = { git = "https://github.com/AdExNetwork/adex-validator-stack-rust", branch = "dev" } +primitives = { git = "https://github.com/AdExNetwork/adex-validator-stack-rust", branch = "bugfixes" } chrono = { version = "0.4" } futures = { version = "0.3" } async-trait = { version = "^0.1" } thiserror = "^1.0" # Server -tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync"] } +tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync", "signal"] } +hyper-tls = "0.4" hyper = { version = "0.13", features = ["stream"] } http = "0.2" @@ -27,8 +28,8 @@ clap = "2.33" toml = "0.5" # Logging -slog = { version = "2.5" } -slog-term = "2.5" +slog = { version = "2.7" } +slog-term = "2.6" slog-async = "2.5" # Other lazy_static = "1.4" diff --git a/README.md b/README.md index 298e4f4..ce663b3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,14 @@ docker build -t adex-supermarket . 2. After building the image you can start a container (`production`): +- with production `https://market.adex.network`: + +``` +docker run --detach -e ENV=production -p 3000:3000 -e MARKET_URL=https://market.adex.network/ adex-supermarket +``` + +- with locally running `adex-market`: + ```bash docker run --detach -e ENV=production -e MARKET_URL=https://localhost:4000 adex-supermarket ``` diff --git a/src/cache.rs b/src/cache.rs index 11ec6a2..a33b689 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -244,6 +244,7 @@ mod test { .expect("Valid URL"); let channel = setup_channel(&leader_url, &follower_url); + let channel_id = channel.id; let leader_id = channel.spec.validators.leader().id; let follower_id = channel.spec.validators.follower().id; @@ -286,7 +287,11 @@ mod test { validator_messages: vec![ValidatorMessage { from: follower_id, received: Utc::now(), - msg: get_new_state_msg().msg, + msg: MessageTypes::NewState(NewState { + signature: String::from("0x0"), + state_root: String::from("0x0"), + balances: expected_balances.clone(), + }), }], }; @@ -316,7 +321,11 @@ mod test { .await; Mock::given(method("GET")) - .and(path("/leader/last-approved")) + .and(path(format!( + "/leader/channel/{}/last-approved", + channel_id + ))) + .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&leader_last_approved)) .expect(2_u64) .mount(&mock_server) @@ -324,9 +333,10 @@ mod test { Mock::given(method("GET")) .and(path(format!( - "/leader/validator-messages/{}/NewState", - leader_id + "/leader/channel/{}/validator-messages/{}/NewState", + channel_id, leader_id ))) + .and(query_param("limit", "1")) .respond_with(ResponseTemplate::new(200).set_body_json(&leader_latest_new_state)) .expect(2_u64) .mount(&mock_server) @@ -340,7 +350,11 @@ mod test { .await; Mock::given(method("GET")) - .and(path("/follower/last-approved")) + .and(path(format!( + "/follower/channel/{}/last-approved", + channel_id + ))) + .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&follower_last_approved)) .expect(2_u64) .mount(&mock_server) @@ -356,7 +370,7 @@ mod test { .get(&channel.id) .expect("This Campaign should exist and should be active"); - assert_eq!(Status::Waiting, active_campaign.status); + assert_eq!(Status::Active, active_campaign.status); assert_eq!(expected_balances, active_campaign.balances); } @@ -418,14 +432,22 @@ mod test { }; Mock::given(method("GET")) - .and(path("/leader/last-approved")) + .and(path(format!( + "/leader/channel/{}/last-approved", + channel_id + ))) + .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&leader_last_approved)) .expect(1_u64) .mount(&mock_server) .await; Mock::given(method("GET")) - .and(path("/follower/last-approved")) + .and(path(format!( + "/follower/channel/{}/last-approved", + channel_id + ))) + .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&follower_last_approved)) .expect(1_u64) .mount(&mock_server) @@ -487,7 +509,11 @@ mod test { }; Mock::given(method("GET")) - .and(path("/leader/last-approved")) + .and(path(format!( + "/leader/channel/{}/last-approved", + channel_id + ))) + .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&leader_last_approved)) .expect(1_u64) .mount(&mock_server) @@ -588,7 +614,10 @@ mod test { .await; Mock::given(method("GET")) - .and(path("/leader/last-approved")) + .and(path(format!( + "/leader/channel/{}/last-approved", + channel_id + ))) .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&leader_last_approved)) // The second time we call is from the Follower Validator to get up to date Status of the Campaign @@ -604,7 +633,10 @@ mod test { .await; Mock::given(method("GET")) - .and(path("/follower/last-approved")) + .and(path(format!( + "/follower/channel/{}/last-approved", + channel_id + ))) .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&follower_last_approved)) .expect(2_u64) @@ -706,7 +738,10 @@ mod test { }; Mock::given(method("GET")) - .and(path("/leader/last-approved")) + .and(path(format!( + "/leader/channel/{}/last-approved", + channel_id + ))) .and(query_param("withHeartbeat", "true")) .respond_with(ResponseTemplate::new(200).set_body_json(&leader_last_approved)) // The second time we call is from the Follower Validator to get up to date Status of the Campaign diff --git a/src/lib.rs b/src/lib.rs index 569153a..e22b6ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,13 @@ #![deny(clippy::all)] #![deny(rust_2018_idioms)] pub use cache::Cache; -use hyper::{client::HttpConnector, Body, Method, Request, Response, Server}; +use hyper::{Body, Client, Method, Request, Response, Server}; +use hyper_tls::HttpsConnector; use std::net::SocketAddr; use std::sync::Arc; use thiserror::Error; -use http::{StatusCode, Uri}; +use http::{header::HOST, StatusCode, Uri}; use slog::{error, info, Logger}; pub mod cache; @@ -25,6 +26,9 @@ pub use sentry_api::SentryApi; pub(crate) static ROUTE_UNITS_FOR_SLOT: &str = "/units-for-slot/"; +// Uses Https Client +type HyperClient = Client>; + #[derive(Debug, Error)] pub enum Error { #[error(transparent)] @@ -56,7 +60,9 @@ pub async fn serve( ) -> Result<(), Error> { use hyper::service::{make_service_fn, service_fn}; - let client = hyper::Client::new(); + let https = HttpsConnector::new(); + let client: HyperClient = Client::builder().build(https); + let market = Arc::new(MarketApi::new(market_url, logger.clone())?); let cache = spawn_fetch_campaigns(logger.clone(), config.clone()).await?; @@ -91,8 +97,10 @@ pub async fn serve( // Then bind and serve... let server = Server::bind(&addr).serve(make_service); + let graceful = server.with_graceful_shutdown(shutdown_signal()); + // And run forever... - if let Err(e) = server.await { + if let Err(e) = graceful.await { error!(&logger, "server error: {}", e); } @@ -103,7 +111,7 @@ async fn handle( mut req: Request, config: Config, cache: Cache, - client: hyper::Client, + client: HyperClient, logger: Logger, market: Arc, ) -> Result, Error> { @@ -114,20 +122,44 @@ async fn handle( get_units_for_slot(&logger, market.clone(), &config, &cache, req).await } (_, method) => { - use http::uri::PathAndQuery; - let method = method.clone(); let path_and_query = req .uri() .path_and_query() - .map(ToOwned::to_owned) - .unwrap_or_else(|| PathAndQuery::from_static("")); + .map(|p_q| { + let string = p_q.to_string(); + // the MarketUrl (i.e. ApiUrl) always suffixes the path + string + .strip_prefix('/') + .map(ToString::to_string) + .unwrap_or(string) + }) + .unwrap_or_default(); let uri = format!("{}{}", market.market_url, path_and_query); *req.uri_mut() = uri.parse::()?; + // for Cloudflare we need to add a HOST header + let market_host_header = { + let url = market.market_url.to_url(); + let host = url + .host_str() + .expect("MarketUrl always has a host") + .to_string(); + + match url.port() { + Some(port) => format!("{}:{}", host, port), + None => host, + } + }; + + let host = market_host_header + .parse() + .expect("The MarketUrl should be valid HOST header"); + req.headers_mut().insert(HOST, host); + let proxy_response = match client.request(req).await { Ok(response) => { info!(&logger, "Proxied request to market"; "uri" => uri, "method" => %method); @@ -146,6 +178,13 @@ async fn handle( } } +async fn shutdown_signal() { + // Wait for the CTRL+C signal + tokio::signal::ctrl_c() + .await + .expect("failed to install CTRL+C signal handler"); +} + async fn spawn_fetch_campaigns( logger: Logger, config: Config, diff --git a/src/main.rs b/src/main.rs index 19c0209..5aa1b5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,14 @@ #![deny(clippy::all)] #![deny(rust_2018_idioms)] -use std::net::SocketAddr; - use clap::{crate_version, App, Arg}; use supermarket::{config::Environment, serve, Config}; use slog::{info, Drain}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; const DEFAULT_PORT: u16 = 3000; +const DEFAULT_IP_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); #[tokio::main] async fn main() -> Result<(), Box> { @@ -46,6 +46,17 @@ async fn main() -> Result<(), Box> { .map(|s| u16::from_str(&s)) .transpose()? .unwrap_or(DEFAULT_PORT); + + let ip_addr = std::env::var("IP_ADDR") + .map(|s| { + s.parse::() + .expect("Invalid Ip address was provided") + }) + .unwrap_or_else(|_| DEFAULT_IP_ADDR); + + // Construct our SocketAddr to listen on... + let socket_addr: SocketAddr = (ip_addr, port).into(); + let config_path = cli.value_of("config"); let config = Config::new(config_path, environment)?; @@ -54,14 +65,12 @@ async fn main() -> Result<(), Box> { info!( &logger, - "ENV: `{}`; PORT: `{}`; {:#?}", environment, port, config + "ENV: `{}`; IP_ADDR: `{}`; PORT: `{}`; {:#?}", environment, ip_addr, port, config ); - // Construct our SocketAddr to listen on... - let addr = SocketAddr::from(([127, 0, 0, 1], port)); - info!(&logger, "Started at: {}", &addr); + info!(&logger, "Web server listening on: {}", &socket_addr); - Ok(serve(addr, logger, market_url, config).await?) + Ok(serve(socket_addr, logger, market_url, config).await?) } pub fn logger() -> slog::Logger { diff --git a/src/market.rs b/src/market.rs index 0ed860f..4248d77 100644 --- a/src/market.rs +++ b/src/market.rs @@ -1,15 +1,13 @@ use primitives::{ - market::{Campaign, StatusType}, - supermarket::units_for_slot::response::AdUnit, - AdSlot, + market::{AdSlotResponse, AdUnitResponse, AdUnitsResponse, Campaign, StatusType}, + util::ApiUrl, + AdSlot, AdUnit, }; use reqwest::{Client, Error, StatusCode}; -use serde::{Deserialize, Serialize}; use slog::{info, Logger}; use std::fmt; -use url::Url; -pub type MarketUrl = Url; +pub type MarketUrl = ApiUrl; pub type Result = std::result::Result; #[derive(Debug, Clone)] @@ -19,19 +17,26 @@ pub struct MarketApi { logger: Logger, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AdSlotResponse { - pub slot: AdSlot, - pub accepted_referrers: Vec, - pub categories: Vec, - pub alexa_rank: Option, +/// Should we query All or only certain statuses +#[derive(Debug)] +pub enum Statuses<'a> { + All, + Only(&'a [StatusType]), } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AdUnitResponse { - pub unit: AdUnit, +impl fmt::Display for Statuses<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Statuses::*; + + match self { + All => write!(f, "all"), + Only(statuses) => { + let statuses = statuses.iter().map(ToString::to_string).collect::>(); + + write!(f, "status={}", statuses.join(",")) + } + } + } } impl MarketApi { @@ -129,9 +134,9 @@ impl MarketApi { let response = self.client.get(url).send().await?; - let ad_units: Vec = response.json().await?; + let ad_units: AdUnitsResponse = response.json().await?; - Ok(ad_units) + Ok(ad_units.0) } pub async fn fetch_campaigns(&self, statuses: &Statuses<'_>) -> Result> { @@ -190,25 +195,3 @@ impl MarketApi { Ok(campaigns) } } - -/// Should we query All or only certain statuses -#[derive(Debug)] -pub enum Statuses<'a> { - All, - Only(&'a [StatusType]), -} - -impl fmt::Display for Statuses<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Statuses::*; - - match self { - All => write!(f, "all"), - Only(statuses) => { - let statuses = statuses.iter().map(ToString::to_string).collect::>(); - - write!(f, "status={}", statuses.join(",")) - } - } - } -} diff --git a/src/sentry_api.rs b/src/sentry_api.rs index c71fb5f..b3f46ef 100644 --- a/src/sentry_api.rs +++ b/src/sentry_api.rs @@ -6,7 +6,7 @@ use primitives::{ ValidatorMessage, ValidatorMessageResponse, }, util::ApiUrl, - Channel, ValidatorDesc, + Channel, ChannelId, ValidatorDesc, }; use reqwest::{Client, Response}; use std::time::Duration; @@ -79,6 +79,7 @@ impl SentryApi { pub async fn get_last_approved( &self, + channel_id: ChannelId, validator: &ValidatorDesc, ) -> Result { // if the validator API URL is wrong, return an error instead of `panic!`ing @@ -86,7 +87,10 @@ impl SentryApi { // if the url is wrong `panic!` let url = api_url - .join("last-approved?withHeartbeat=true") + .join(&format!( + "channel/{}/last-approved?withHeartbeat=true", + channel_id + )) .expect("Url should be valid"); Ok(self.client.get(url).send().await?.json().await?) @@ -94,11 +98,13 @@ impl SentryApi { pub async fn get_latest_new_state( &self, + channel_id: ChannelId, validator: &ValidatorDesc, ) -> Result, Error> { let url = &format!( - "{}/validator-messages/{}/NewState?limit=1", + "{}/channel/{}/validator-messages/{}/NewState?limit=1", validator.url.trim_end_matches('/'), + channel_id, validator.id ); diff --git a/src/status.rs b/src/status.rs index 29f69bf..4699542 100644 --- a/src/status.rs +++ b/src/status.rs @@ -172,7 +172,7 @@ pub async fn is_finalized(sentry: &SentryApi, channel: &Channel) -> Result true, + None => true, + _ => false, + }; + // isActive & isReady (we don't need distinguish between Active & Ready here) - if is_active(&messages) && is_ready(&messages) { + if is_active(&messages) || (is_ready(&messages) && channel_active_from) { Ok((Status::Active, messages.get_leader_new_state_balances())) } else { Ok((Status::Waiting, messages.get_leader_new_state_balances())) @@ -271,7 +277,7 @@ pub async fn get_status( /// Calls SentryApi for the Leader's LastApproved NewState and returns the NewState Balance async fn fetch_balances(sentry: &SentryApi, channel: &Channel) -> Result { let leader_la = sentry - .get_last_approved(&channel.spec.validators.leader()) + .get_last_approved(channel.id, &channel.spec.validators.leader()) .await?; let balances = leader_la @@ -324,7 +330,7 @@ async fn is_rejected_state( }; let latest_new_state = sentry - .get_latest_new_state(channel.spec.validators.leader()) + .get_latest_new_state(channel.id, channel.spec.validators.leader()) .await?; let latest_new_state = match latest_new_state { diff --git a/src/units_for_slot.rs b/src/units_for_slot.rs index 9622ea8..41bdb6d 100644 --- a/src/units_for_slot.rs +++ b/src/units_for_slot.rs @@ -1,20 +1,19 @@ use crate::{ cache::{Cache, Campaign, Client}, - market::AdSlotResponse, not_found, service_unavailable, status::Status, Config, Error, MarketApi, ROUTE_UNITS_FOR_SLOT, }; use chrono::Utc; -use http::header::HeaderName; +use http::header::{HeaderName, CONTENT_TYPE}; use hyper::{header::USER_AGENT, Body, Request, Response}; use input::Input; use primitives::{ + market::AdSlotResponse, supermarket::units_for_slot::response, - supermarket::units_for_slot::response::{AdUnit, Response as UnitsForSlotResponse}, - targeting::eval_with_callback, - targeting::{get_pricing_bounds, input, Output}, - ValidatorId, + supermarket::units_for_slot::response::Response as UnitsForSlotResponse, + targeting::{eval_with_callback, get_pricing_bounds, input, Output}, + AdUnit, ValidatorId, }; use slog::{error, info, warn, Logger}; use std::sync::Arc; @@ -71,7 +70,7 @@ pub async fn get_units_for_slot( }; let accepted_referrers = ad_slot_response.accepted_referrers.clone(); - let units_ipfses: Vec = units.iter().map(|au| au.id.to_string()).collect(); + let units_ipfses: Vec = units.iter().map(|au| au.ipfs.to_string()).collect(); let fallback_unit: Option = match ad_slot_response.slot.fallback_unit.as_ref() { Some(unit_ipfs) => { let ad_unit_response = match market.fetch_unit(&unit_ipfs).await { @@ -170,7 +169,7 @@ pub async fn get_units_for_slot( let campaigns_limited_by_earner = get_campaigns(cache, config, &deposit_assets, publisher_id).await; - info!(&logger, "Fetched Cache campaigns"; "length" => campaigns_limited_by_earner.len(), "publisher_id" => %publisher_id); + info!(&logger, "Fetched Cache campaigns limited by earner (publisher)"; "length" => campaigns_limited_by_earner.len(), "publisher_id" => %publisher_id); // We return those in the result (which means AdView would have those) but we don't actually use them // we do that in order to have the same variables as the validator, so that the `price` is the same @@ -186,11 +185,10 @@ pub async fn get_units_for_slot( ad_slot_id: ad_slot_response.slot.ipfs.clone(), ad_slot_type: ad_slot_response.slot.ad_type.clone(), publisher_id, - country: country.clone(), + country, event_type: "IMPRESSION".to_string(), - // TODO: Replace with [unsigned_abs](https://doc.rust-lang.org/std/primitive.i64.html#method.unsigned_abs) once it's stabilized seconds_since_epoch: Utc::now(), - user_agent_os: user_agent_os.clone(), + user_agent_os, user_agent_browser_family: user_agent_browser_family.clone(), }, ad_unit_id: None, @@ -214,10 +212,14 @@ pub async fn get_units_for_slot( targeting_input_base, accepted_referrers, campaigns, - fallback_unit, + fallback_unit: fallback_unit.map(|ad_unit| response::AdUnit::from(&ad_unit)), }; - Ok(Response::new(Body::from(serde_json::to_string(&response)?))) + Ok(Response::builder() + .status(http::StatusCode::OK) + .header(CONTENT_TYPE, "application/json") + .body(Body::from(serde_json::to_string(&response)?)) + .expect("Should create response")) } } @@ -236,7 +238,8 @@ async fn get_campaigns( // The Supermarket has the Active status combining Active & Ready from Market if campaign.status == Status::Active && campaign.channel.creator != publisher_id - && deposit_assets.contains(&campaign.channel.deposit_asset) + && (deposit_assets.is_empty() + || deposit_assets.contains(&campaign.channel.deposit_asset)) { Some(campaign) } else { diff --git a/src/units_for_slot_test.rs b/src/units_for_slot_test.rs index 40eb0b9..d4797cc 100644 --- a/src/units_for_slot_test.rs +++ b/src/units_for_slot_test.rs @@ -5,16 +5,13 @@ use http::request::Request; use hyper::Body; use primitives::{ supermarket::units_for_slot::response::{ - AdUnit, Campaign as ResponseCampaign, Channel as ResponseChannel, UnitsWithPrice, + Campaign as ResponseCampaign, Channel as ResponseChannel, UnitsWithPrice, }, targeting::{input, Function, Rule, Value}, - util::tests::prep_db::{DUMMY_CHANNEL, IDS}, - AdSlot, BigNum, IPFS, + util::tests::prep_db::{DUMMY_AD_UNITS, DUMMY_CHANNEL, IDS}, + AdSlot, BigNum, }; -use std::iter::Iterator; -use std::str::FromStr; -use std::sync::Arc; -use std::{collections::HashMap, convert::TryFrom}; +use std::{collections::HashMap, iter::Iterator, str::FromStr, sync::Arc}; use url::Url; use wiremock::{ matchers::{method, path}, @@ -22,10 +19,11 @@ use wiremock::{ }; mod units_for_slot_tests { + use super::*; use chrono::DateTime; use http::header::USER_AGENT; - use primitives::{Channel, ChannelId}; + use primitives::{market::AdUnitsResponse, targeting::Rules, Channel, ChannelId}; // User Agent OS: Linux (only in `woothee`) // User Agent Browser Family: Firefox @@ -34,8 +32,9 @@ mod units_for_slot_tests { // uses two-letter country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 const TEST_CLOUDFLARE_IPCOUNTY: &str = "BG"; - fn get_mock_campaign(channel: Channel, units: &[AdUnit]) -> ResponseCampaign { - let units_with_price = get_units_with_price(&channel, &units); + /// Uses the Channel AdUnits as UnitsWithPrice for the response + fn get_mock_campaign(channel: Channel) -> ResponseCampaign { + let units_with_price = get_units_with_price(&channel); let targeting_rules = channel.spec.targeting_rules.clone(); ResponseCampaign { channel: ResponseChannel::from(channel), @@ -44,50 +43,18 @@ mod units_for_slot_tests { } } - fn get_units_with_price(channel: &Channel, units: &[AdUnit]) -> Vec { - units + fn get_units_with_price(channel: &Channel) -> Vec { + channel + .spec + .ad_units .iter() - .cloned() .map(|u| UnitsWithPrice { - unit: u, + unit: u.into(), price: channel.spec.min_per_impression.clone(), }) .collect() } - fn get_supermarket_ad_units() -> Vec { - vec![ - AdUnit { - id: IPFS::try_from("Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f") - .expect("should convert"), - media_url: "ipfs://QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://www.adex.network/?stremio-test-banner-1".to_string(), - }, - AdUnit { - id: IPFS::try_from("QmVhRDGXoM3Fg3HZD5xwMuxtb9ZErwC8wHt8CjsfxaiUbZ") - .expect("should convert"), - media_url: "ipfs://QmQB7uz7Gxfy7wqAnrnBcZFaVJLos8J9gn8mRcHQU6dAi1".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://www.adex.network/?adex-campaign=true&pub=stremio".to_string(), - }, - AdUnit { - id: IPFS::try_from("QmYwcpMjmqJfo9ot1jGe9rfXsszFV1WbEA59QS7dEVHfJi") - .expect("should convert"), - media_url: "ipfs://QmQB7uz7Gxfy7wqAnrnBcZFaVJLos8J9gn8mRcHQU6dAi1".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://www.adex.network/?adex-campaign=true".to_string(), - }, - AdUnit { - id: IPFS::try_from("QmTAF3FsFDS7Ru8WChoD9ofiHTH8gAQfR4mYSnwxqTDpJH") - .expect("should convert"), - media_url: "ipfs://QmQAcfBJpDDuH99A4p3pFtUmQwamS8UYStP5HxHC7bgYXY".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://adex.network".to_string(), - }, - ] - } - fn get_mock_rules(categories: &[&str]) -> Vec { let get_rule = Function::new_get("adSlot.categories"); let categories_array = @@ -166,80 +133,16 @@ mod units_for_slot_tests { } } - fn mock_channel_units() -> Vec { - vec![ - primitives::AdUnit { - ipfs: IPFS::try_from("Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f") - .expect("should convert"), - media_url: "ipfs://QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://www.adex.network/?stremio-test-banner-1".to_string(), - archived: false, - description: Some("test description".to_string()), - ad_type: "legacy_250x250".to_string(), - created: Utc.timestamp(1_564_383_600, 0), - min_targeting_score: Some(1.00), - modified: None, - owner: IDS["publisher"], - title: Some("test title".to_string()), - }, - primitives::AdUnit { - ipfs: IPFS::try_from("QmVhRDGXoM3Fg3HZD5xwMuxtb9ZErwC8wHt8CjsfxaiUbZ") - .expect("should convert"), - media_url: "ipfs://QmQB7uz7Gxfy7wqAnrnBcZFaVJLos8J9gn8mRcHQU6dAi1".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://www.adex.network/?adex-campaign=true&pub=stremio".to_string(), - archived: false, - description: Some("test description".to_string()), - ad_type: "legacy_250x250".to_string(), - created: Utc.timestamp(1_564_383_600, 0), - min_targeting_score: Some(1.00), - modified: None, - owner: IDS["publisher"], - title: Some("test title".to_string()), - }, - primitives::AdUnit { - ipfs: IPFS::try_from("QmYwcpMjmqJfo9ot1jGe9rfXsszFV1WbEA59QS7dEVHfJi") - .expect("should convert"), - media_url: "ipfs://QmQB7uz7Gxfy7wqAnrnBcZFaVJLos8J9gn8mRcHQU6dAi1".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://www.adex.network/?adex-campaign=true".to_string(), - archived: false, - description: Some("test description".to_string()), - ad_type: "legacy_250x250".to_string(), - created: Utc.timestamp(1_564_383_600, 0), - min_targeting_score: Some(1.00), - modified: None, - owner: IDS["publisher"], - title: Some("test title".to_string()), - }, - primitives::AdUnit { - ipfs: IPFS::try_from("QmTAF3FsFDS7Ru8WChoD9ofiHTH8gAQfR4mYSnwxqTDpJH") - .expect("should convert"), - media_url: "ipfs://QmQAcfBJpDDuH99A4p3pFtUmQwamS8UYStP5HxHC7bgYXY".to_string(), - media_mime: "image/jpeg".to_string(), - target_url: "https://adex.network".to_string(), - archived: false, - description: Some("test description".to_string()), - ad_type: "legacy_250x250".to_string(), - created: Utc.timestamp(1_564_383_600, 0), - min_targeting_score: Some(1.00), - modified: None, - owner: IDS["publisher"], - title: Some("test title".to_string()), - }, - ] - } - fn mock_channel(rules: &[Rule]) -> Channel { let mut channel = DUMMY_CHANNEL.clone(); - channel.spec.ad_units = mock_channel_units(); + channel.spec.ad_units = DUMMY_AD_UNITS.to_vec(); // NOTE: always set the spec.targeting_rules first - channel.spec.targeting_rules = rules.to_vec(); + channel.spec.targeting_rules = Rules(rules.to_vec()); channel.spec.min_per_impression = 100_000_000_000_000.into(); channel.spec.max_per_impression = 1_000_000_000_000_000.into(); - channel.spec.active_from = Some(Utc.timestamp_millis(1_606_136_400_000)); + // Timestamp: 1_606_136_400_000 + channel.spec.active_from = Some(Utc.ymd(2020, 11, 23).and_hms(15, 0, 0)); channel } @@ -311,12 +214,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await; @@ -325,7 +236,8 @@ mod units_for_slot_tests { .respond_with(ResponseTemplate::new(200).set_body_json(&mock_slot)) .mount(&server) .await; - let campaign = get_mock_campaign(channel.clone(), &ad_units); + + let campaign = get_mock_campaign(channel.clone()); let request = Request::get(format!( "/units-for-slot/{}?depositAsset={}", @@ -403,12 +315,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await; @@ -483,6 +403,7 @@ mod units_for_slot_tests { let rules = get_mock_rules(&categories); let mut channel = mock_channel(&rules); channel.creator = IDS["publisher"]; + let config = crate::config::DEVELOPMENT.clone(); let mock_client = MockClient::init( vec![mock_cache_campaign(channel.clone(), Status::Active)], @@ -493,12 +414,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await; @@ -585,12 +514,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units: Vec = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await; @@ -675,12 +612,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await; @@ -765,12 +710,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await; @@ -862,12 +815,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await; @@ -876,7 +837,7 @@ mod units_for_slot_tests { .respond_with(ResponseTemplate::new(200).set_body_json(&mock_slot)) .mount(&server) .await; - let campaign = get_mock_campaign(channel.clone(), &ad_units); + let campaign = get_mock_campaign(channel.clone()); let request = Request::get(format!( "/units-for-slot/{}?depositAsset={}", @@ -955,12 +916,20 @@ mod units_for_slot_tests { let mock_cache = Cache::initialize(mock_client).await; - let ad_units = get_supermarket_ad_units(); + let market_ad_units = AdUnitsResponse( + channel + .spec + .ad_units + .clone() + .into_iter() + .map(Into::into) + .collect(), + ); let mock_slot = get_supermarket_ad_slot(&rules, &categories); Mock::given(method("GET")) .and(path("/market/units")) - .respond_with(ResponseTemplate::new(200).set_body_json(&ad_units)) + .respond_with(ResponseTemplate::new(200).set_body_json(&market_ad_units)) .mount(&server) .await;