diff --git a/Cargo.lock b/Cargo.lock index 75b85ae8d..355781bdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,14 +65,21 @@ name = "adview-serve" version = "0.1.0" dependencies = [ "adview-manager", + "anyhow", "axum", + "axum-extra", "chrono", - "env_logger", - "log", + "envy", + "once_cell", + "pretty_assertions", "primitives", + "reqwest", "serde", + "serde_json", "tera", "tokio", + "tracing", + "tracing-subscriber", "wiremock", ] @@ -137,6 +144,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "anyhow" version = "1.0.65" @@ -329,6 +345,7 @@ checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bitflags", "bytes", "futures-util", @@ -369,6 +386,39 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-extra" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb" +dependencies = [ + "axum", + "bytes", + "futures-util", + "http", + "mime", + "pin-project-lite", + "serde", + "serde_html_form", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6293dae2ec708e679da6736e857cf8532886ef258e92930f38279c12641628b8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "axum-server" version = "0.4.2" @@ -1147,19 +1197,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "envy" version = "0.4.2" @@ -1616,6 +1653,12 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1731,12 +1774,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.20" @@ -2044,6 +2081,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.9" @@ -2791,6 +2837,21 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "platform" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "dashmap", + "pretty_assertions", + "primitives", + "serde", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "polling" version = "2.3.0" @@ -3219,6 +3280,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.27" @@ -3596,6 +3666,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_html_form" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3312ac3bf56e70cb7082a85db89d127940607b6acf39bd537cc06c8212124927" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa 1.0.4", + "ryu", + "serde", +] + [[package]] name = "serde_json" version = "1.0.86" @@ -3773,6 +3856,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -4381,9 +4473,21 @@ dependencies = [ "cfg-if", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.30" @@ -4391,6 +4495,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +dependencies = [ + "ansi_term", + "matchers", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec 1.10.0", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -4585,6 +4732,12 @@ dependencies = [ "wiremock", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "value-bag" version = "1.0.0-alpha.9" diff --git a/Cargo.toml b/Cargo.toml index 0b1d232d1..6c66bd78b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,6 @@ members = [ "validator_worker", "sentry", "test_harness", + # for mocking calls to the platform + "test_harness/platform", ] diff --git a/adview-manager/README.md b/adview-manager/README.md index fe6f385e1..e1e7f83e1 100644 --- a/adview-manager/README.md +++ b/adview-manager/README.md @@ -12,7 +12,7 @@ a publisher websites. Running the local server: -`RUST_LOG=debug cargo run -p serve` +`RUST_LOG=debug cargo run -p adview-serve` This will start a server at `127.0.0.1:3030` @@ -21,4 +21,16 @@ for the server. Routes: -- `GET /ad` - visualizes a single ad \ No newline at end of file +- `GET /` - landing page +- `GET /preview` - preview form for submitting a single AdSlot response from `platform` and see the result +- `POST /preview` - preview of a single AdSlot Image example +- `GET /preview/ad` - preview a single example ad +- `GET /preview/video` - preview a single Video example ad + + +### POST `/preview` + +You need to be running: +- `sentry` Leader at 8005 +- `sentry` Follower at 8006 +- Mock [`Platform`](../test_harness/platform) at 8004 \ No newline at end of file diff --git a/adview-manager/serve/Cargo.toml b/adview-manager/serve/Cargo.toml index 2f7f0c7fd..bc6b44482 100644 --- a/adview-manager/serve/Cargo.toml +++ b/adview-manager/serve/Cargo.toml @@ -12,13 +12,21 @@ publish = false # Domain adex_primitives = { version = "0.2.0", path = "../../primitives", package = "primitives", features = ["test-util"] } adview-manager = { path = "../" } + +# Application errors +anyhow = "1" +# Time chrono = "0.4" +# Making requests to the Mocked Platform +reqwest = { version = "0.11", features = ["json"] } + # Async runtime tokio = { version = "1", features = ["macros", "time", "rt-multi-thread"] } # Web Server -axum = "0.5" +axum = { version = "0.5", features = ["headers", "macros"] } +axum-extra = { version = "0.3", features = ["form"] } # Template engine tera = { version = "1" } @@ -28,7 +36,16 @@ wiremock = { version = "0.5" } # (De)Serialization serde = { version = "^1.0", features = ["derive"] } +serde_json = "1" + +# For env. variable deserialization for Config +envy = "0.4" + +# Tracing +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +once_cell = "1" -# Logging -log = "0.4" -env_logger = { version = "0.9" } +[dev-dependencies] +pretty_assertions = "1" diff --git a/adview-manager/serve/src/app.rs b/adview-manager/serve/src/app.rs index 50c239006..d4588edd3 100644 --- a/adview-manager/serve/src/app.rs +++ b/adview-manager/serve/src/app.rs @@ -1,11 +1,19 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, +}; -use axum::{routing::get, Extension, Router, Server}; -use log::info; +use axum::{http::StatusCode, response::IntoResponse, routing::get, Extension, Router, Server}; +use serde::Deserialize; +use tracing::{error, info}; use tera::Tera; -use crate::routes::{get_index, get_preview_ad, get_preview_video}; +use crate::routes::{ + get_index, get_preview_ad, get_preview_video, get_slot_preview_form, post_slot_preview, +}; + +pub use envy::Error as EnvError; #[derive(Debug)] pub struct State { @@ -13,8 +21,10 @@ pub struct State { } pub struct Application { - /// The shared state of the application + /// The shared state of the application. state: Arc, + /// Configuration values taken from the environment variables. + env_config: EnvConfig, } impl Application { @@ -34,25 +44,32 @@ impl Application { // Use globbing let tera = Tera::new(&templates_glob)?; + let env_config = EnvConfig::from_env()?; + let shared_state = Arc::new(State { tera }); Ok(Self { state: shared_state, + env_config, }) } pub async fn run(&self) -> Result<(), Box> { + let preview_routes = Router::new() + .route("/", get(get_slot_preview_form).post(post_slot_preview)) + .route("/ad", get(get_preview_ad)) + .route("/video", get(get_preview_video)); + // build our application with a single route let app = Router::new() .route("/", get(get_index)) - .route("/preview/ad", get(get_preview_ad)) - .route("/preview/video", get(get_preview_video)) + .nest("/preview", preview_routes) .layer(Extension(self.state.clone())); - let socket_addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); + let socket_addr: SocketAddr = SocketAddr::new(self.env_config.ip, self.env_config.port); info!("Server running on: {socket_addr}"); - // run it with hyper on localhost:3030 + // run it with hyper on the socket address Server::bind(&socket_addr) .serve(app.into_make_service()) .await?; @@ -60,3 +77,85 @@ impl Application { Ok(()) } } + +pub struct Error { + error: Box, + status: StatusCode, +} + +impl Error { + pub fn new(error: E, status: StatusCode) -> Self + where + E: Into>, + { + Self { + error: error.into(), + status, + } + } + + /// Create a new [`Error`] from [`anyhow::Error`] with a custom [`StatusCode`] + /// instead of the default [`StatusCode::INTERNAL_SERVER_ERROR`]. + pub fn anyhow_status(error: anyhow::Error, status: StatusCode) -> Self { + Self { + error: error.into(), + status, + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let response_tuple = match self.status { + StatusCode::INTERNAL_SERVER_ERROR => { + error!({error = %self.error}, "Server error"); + (StatusCode::INTERNAL_SERVER_ERROR, self.error.to_string()) + } + // we want to log any error that is with status > 500 + status_code if status_code.as_u16() > 500 => { + error!({error = %self.error}, "Server error"); + (status_code, self.error.to_string()) + } + // anything else is < 500, so it's safe to not log it due to e.g. bad user input + status_code => (status_code, self.error.to_string()), + }; + + response_tuple.into_response() + } +} + +impl From for Error +where + E: Into, +{ + fn from(err: E) -> Self { + let anyhow_err: anyhow::Error = err.into(); + + Self { + error: anyhow_err.into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Deserialize, Debug, Clone, Copy)] +pub struct EnvConfig { + #[serde(default = "EnvConfig::default_ip")] + ip: IpAddr, + #[serde(default = "EnvConfig::default_port")] + port: u16, +} + +impl EnvConfig { + pub fn from_env() -> Result { + envy::from_env() + } + + pub fn default_port() -> u16 { + 8001 + } + + pub fn default_ip() -> IpAddr { + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) + } +} diff --git a/adview-manager/serve/src/main.rs b/adview-manager/serve/src/main.rs index 8e779e022..f9ce8c924 100644 --- a/adview-manager/serve/src/main.rs +++ b/adview-manager/serve/src/main.rs @@ -1,8 +1,12 @@ use adview_serve::app::Application; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; #[tokio::main] async fn main() -> Result<(), Box> { - env_logger::init(); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); Application::new()?.run().await?; diff --git a/adview-manager/serve/src/routes.rs b/adview-manager/serve/src/routes.rs index 1c3891d98..022d92e89 100644 --- a/adview-manager/serve/src/routes.rs +++ b/adview-manager/serve/src/routes.rs @@ -1,26 +1,30 @@ -use std::sync::Arc; +use std::{fmt::Display, ops::Deref, str::FromStr, sync::Arc}; + +use anyhow::{anyhow, bail}; +use axum::{ + http::{header::ACCEPT_LANGUAGE, HeaderMap, StatusCode}, + response::Html, + Extension, +}; +use axum_extra::extract::Form; +use chrono::{TimeZone, Utc}; +use reqwest::Client; +use serde::{de::Error as _, Deserialize, Deserializer, Serialize}; +use tera::Context; use adex_primitives::{ - sentry::{units_for_slot, IMPRESSION}, - targeting::{input::Global, Input}, - test_util::{DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, DUMMY_VALIDATOR_LEADER}, + platform::{AdSlotResponse, Website}, + targeting::Rules, + test_util::{ + DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, + DUMMY_VALIDATOR_LEADER, IDS, PUBLISHER, WHITELISTED_TOKENS, + }, util::ApiUrl, - ToHex, + AdSlot, Address, }; -use adview_manager::{ - get_unit_html_with_events, manager::Size, manager::DEFAULT_TOKENS, Manager, Options, -}; -use axum::{response::Html, Extension}; -use chrono::Utc; -use log::debug; -use wiremock::{ - matchers::{method, path, query_param}, - Mock, MockServer, ResponseTemplate, -}; - -use tera::Context; +use adview_manager::{get_unit_html_with_events, manager::Size, Manager, Options}; -use crate::app::State; +use crate::app::{Error, State}; /// `GET /` pub async fn get_index(Extension(state): Extension>) -> Html { @@ -34,85 +38,29 @@ pub async fn get_index(Extension(state): Extension>) -> Html /// `GET /preview/ad` pub async fn get_preview_ad(Extension(state): Extension>) -> Html { - // For mocking the `get_units_for_slot_resp` call - let mock_server = MockServer::start().await; - - let market_url = mock_server.uri().parse().unwrap(); - let whitelisted_tokens = DEFAULT_TOKENS.clone(); let disabled_video = false; - let publisher_addr = "0x0000000000000000626f62627973686d75726461" - .parse() - .unwrap(); + let publisher_addr = *PUBLISHER; + let campaign = DUMMY_CAMPAIGN.clone(); + // ordering matters + let validators_url = vec![ + ApiUrl::parse(&campaign.leader().unwrap().url).expect("should parse"), + ApiUrl::parse(&campaign.leader().unwrap().url).expect("should parse"), + ]; let options = Options { - market_url, market_slot: DUMMY_IPFS[0], publisher_addr, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared - whitelisted_tokens, + whitelisted_tokens: WHITELISTED_TOKENS.clone(), size: Some(Size::new(300, 100)), - // TODO: Check this value navigator_language: Some("bg".into()), /// Defaulted disabled_video, disabled_sticky: false, - validators: vec![ - ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"), - ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"), - ], + validators: validators_url, }; - let manager = - Manager::new(options.clone(), Default::default()).expect("Failed to create AdView Manager"); - let pub_prefix = publisher_addr.to_hex(); - - let units_for_slot_resp = units_for_slot::response::Response { - targeting_input_base: Input { - ad_view: None, - global: Global { - ad_slot_id: options.market_slot, - ad_slot_type: "".into(), - publisher_id: publisher_addr, - country: Some("Bulgaria".into()), - event_type: IMPRESSION, - seconds_since_epoch: Utc::now(), - user_agent_os: None, - user_agent_browser_family: None, - }, - campaign: None, - balances: None, - ad_unit_id: None, - ad_slot: None, - }, - accepted_referrers: vec![], - fallback_unit: None, - campaigns: vec![], - }; - - // Mock the `get_units_for_slot_resp` call - let mock_call = Mock::given(method("GET")) - .and(path(format!("units-for-slot/{}", options.market_slot))) - // pubPrefix=HEX&depositAssets[]=0xASSET1&depositAssets[]=0xASSET2 - .and(query_param("pubPrefix", pub_prefix)) - .and(query_param( - "depositAssets[]", - "0x6B175474E89094C44Da98b954EedeAC495271d0F", - )) - .respond_with(ResponseTemplate::new(200).set_body_json(units_for_slot_resp)) - .expect(1) - .named("get_units_for_slot_resp"); - - // Mounting the mock on the mock server - it's now effective! - mock_call.mount(&mock_server).await; - - let demand_resp = manager - .get_units_for_slot_resp() - .await - .expect("Should return Mocked response"); - - debug!("Mocked response: {demand_resp:?}"); - - let supermarket_ad_unit = adex_primitives::sentry::units_for_slot::response::AdUnit { + let ufs_ad_unit = adex_primitives::sentry::units_for_slot::response::AdUnit { /// Same as `ipfs` ipfs: DUMMY_IPFS[1], media_url: "ipfs://QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR".to_string(), @@ -122,10 +70,10 @@ pub async fn get_preview_ad(Extension(state): Extension>) -> Html>) -> Html>) -> Html { - let whitelisted_tokens = DEFAULT_TOKENS.clone(); let disabled_video = false; - let publisher_addr = "0x0000000000000000626f62627973686d75726461" - .parse() - .unwrap(); + let publisher_addr = *PUBLISHER; let options = Options { - market_url: "http://placeholder.com".parse().unwrap(), market_slot: DUMMY_IPFS[0], publisher_addr, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared - whitelisted_tokens, + whitelisted_tokens: WHITELISTED_TOKENS.clone(), size: Some(Size::new(728, 90)), - // TODO: Check this value navigator_language: Some("bg".into()), /// Defaulted disabled_video, @@ -188,7 +131,6 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< false, ); - // let video_html = get_unit_html_with_events(&options, ); let html = { let mut context = Context::new(); context.insert("ad_code", &video_html); @@ -201,3 +143,353 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< Html(html) } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdSlotPreview { + #[serde(with = "form_json")] + adslot_response: AdSlotResponse, + #[serde(default)] + disabled_video: bool, + #[serde(default)] + disabled_sticky: bool, + publisher: Address, + #[serde(deserialize_with = "empty_field_string::<_, ApiUrl>")] + validators: Vec, + #[serde(deserialize_with = "empty_field_string::<_, Address>")] + whitelisted_tokens: Vec
, +} + +mod form_json { + use serde::{ + de::{DeserializeOwned, Error as _}, + ser::Error as _, + Deserialize, Deserializer, Serialize, Serializer, + }; + + pub fn serialize(value: &T, serializer: S) -> Result + where + S: Serializer, + T: Serialize, + { + let json = serde_json::to_string(value).map_err(S::Error::custom)?; + serializer.serialize_str(&json) + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result + where + D: Deserializer<'de>, + T: DeserializeOwned, + { + let json = String::deserialize(deserializer)?; + + serde_json::from_str::(&json).map_err(D::Error::custom) + } +} + +pub fn empty_field_string<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + ::Err: Display, +{ + let vec_of_string = >::deserialize(deserializer)?; + + vec_of_string + .into_iter() + .filter_map(|string| { + if string.is_empty() { + None + } else { + Some(string.parse::()) + } + }) + .collect::>() + .map_err(D::Error::custom) +} + +/// `GET /preview` +/// +/// Shows a form to submit a JSON [`AdSlot`] Response ([`AdSlotResponse`]) +/// from the platform and preview the html with the matching of an [`AdUnit`] +/// using the manager. +/// +/// Uses the Ganache config to select all the whitelisted tokens in all chains. +/// It's configured to use locally running sentry validators at ports `8005` and `8006`. +/// Alongside locally running mocked platform at `8004`. +/// +/// [`AdUnit`]: adex_primitives::AdUnit +#[axum::debug_handler] +pub async fn get_slot_preview_form( + Extension(state): Extension>, +) -> Result, Error> { + let validators = vec![ + ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"), + ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"), + ]; + + let ad_slot = AdSlot { + ipfs: DUMMY_IPFS[0], + ad_type: "legacy_300x100".to_string(), + min_per_impression: None, + rules: Rules::default(), + fallback_unit: Some(DUMMY_AD_UNITS[0].ipfs), + owner: IDS[&PUBLISHER], + created: Utc.ymd(2019, 7, 29).and_hms(7, 0, 0), + title: Some("Test slot 1".to_string()), + description: Some("Test slot for running integration tests".to_string()), + website: Some("https://adex.network".to_string()), + archived: false, + modified: Some(Utc.ymd(2019, 7, 29).and_hms(7, 0, 0)), + }; + + let adslot_response = AdSlotResponse { + slot: ad_slot, + fallback: Some(DUMMY_AD_UNITS[0].clone()), + website: Some(Website { + categories: vec![ + "IAB3".to_string(), + "IAB13-7".to_string(), + "IAB5".to_string(), + ], + accepted_referrers: vec![], + }), + }; + + let html = { + let mut context = Context::new(); + context.insert("default_publisher", &*PUBLISHER); + context.insert("default_validators", &validators); + context.insert("default_adslot_response", &adslot_response); + context.insert("default_whitelisted_tokens", WHITELISTED_TOKENS.deref()); + + state + .tera + .render("preview_form.html", &context) + .expect("Should render") + }; + + Ok(Html(html)) +} + +/// `POST /preview` +/// +/// Uses the provided with the POST data and gets a matching [`AdUnit`] html +/// with the manager. +/// +/// Uses the Ganache config to select all the whitelisted tokens in all chains. +/// It's configured to use locally running sentry validators at ports `8005` and `8006`. +/// Alongside locally running mocked platform at `8004`. +/// +/// [`AdUnit`]: adex_primitives::AdUnit +#[axum::debug_handler] +pub async fn post_slot_preview( + Extension(state): Extension>, + headers: HeaderMap, + Form(adslot_preview): Form, +) -> Result, Error> { + let ad_slot = adslot_preview.adslot_response.slot.clone(); + + // setup the `AdSlotResponse` from the Platform + setup_platform_response(&adslot_preview.adslot_response).await?; + + // extracted from Accept-language header + let navigator_language = headers + .get(ACCEPT_LANGUAGE) + .map(|value| value.to_str()) + .transpose()? + .map(parse_navigator_language) + .transpose()? + .flatten() + // TODO: make configurable? + .unwrap_or_else(|| "en".into()); + + let options = Options { + market_slot: ad_slot.ipfs, + publisher_addr: *PUBLISHER, + // All passed tokens must be of the same price, so that the amounts can be accurately compared + whitelisted_tokens: adslot_preview.whitelisted_tokens.into_iter().collect(), + size: Some( + size_from_type(&ad_slot.ad_type) + .map_err(|error| Error::anyhow_status(error, StatusCode::BAD_REQUEST))?, + ), + navigator_language: Some(navigator_language), + /// Defaulted + disabled_video: adslot_preview.disabled_video, + disabled_sticky: adslot_preview.disabled_sticky, + validators: adslot_preview.validators, + }; + + let manager = Manager::new(options, Default::default())?; + + let next_ad_unit = manager.get_next_ad_unit().await?; + + let html = { + let mut context = Context::new(); + context.insert("next_ad_unit", &next_ad_unit); + context.insert("ad_slot", &ad_slot); + + state + .tera + .render("next_ad.html", &context) + .expect("Should render") + }; + + Ok(Html(html)) +} + +/// It takes a type like `legacy_300x250` and returns a [`Size`] struct with size of `300x250`. +fn size_from_type(ad_type: &str) -> anyhow::Result { + let size_str = ad_type + .strip_prefix("legacy_") + .ok_or_else(|| anyhow!("Missing `legacy_` prefix"))?; + + let (width_str, height_str) = size_str + .split_once('x') + .ok_or_else(|| anyhow!("Width and height should be separated by `x`"))?; + + match (width_str.parse::(), height_str.parse::()) { + (Ok(width), Ok(height)) => Ok(Size::new(width, height)), + (Ok(_), Err(_err)) => bail!("Height `{height_str}` failed to be parse as u64"), + (Err(_err), Ok(_)) => bail!("Width `{width_str}` failed to be parse as u64"), + (Err(_), Err(_)) => { + bail!("Width `{width_str}` and Height `{height_str}` failed to be parse as u64") + } + } +} + +/// This function is flawed because we don't parse the language itself and we +/// don't take the weights into account! +fn parse_navigator_language(accept_language: &str) -> anyhow::Result> { + if accept_language == "*" { + return Ok(None); + } + + let first_language = accept_language + .chars() + .take_while(|ch| ch != &',' && ch != &';') + .collect::(); + + if first_language.is_empty() { + return Ok(None); + } + + Ok(Some(first_language)) +} + +async fn setup_platform_response(response: &AdSlotResponse) -> anyhow::Result<()> { + let client = Client::builder().build()?; + + client + .post("http://localhost:8004/slot") + .json(&response) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use adview_manager::manager::Size; + + use super::{parse_navigator_language, size_from_type}; + + #[test] + fn test_parse_navigator_language() { + { + let any_language = "*"; + let result = parse_navigator_language(any_language).expect("Should parse language"); + + assert_eq!(None, result, "Any language should be matched"); + } + + { + let result = parse_navigator_language("").expect("Should parse language"); + + assert_eq!(None, result, "No language"); + } + + { + let multiple_with_factor = "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5"; + let result = + parse_navigator_language(multiple_with_factor).expect("Should parse language"); + + assert_eq!( + Some("fr-CH".to_string()), + result, + "fr-CH language should be matched" + ); + } + + { + let english_with_factor = "en-US,en;q=0.5"; + let result = + parse_navigator_language(english_with_factor).expect("Should parse language"); + + assert_eq!( + Some("en-US".to_string()), + result, + "en-US language should be matched" + ); + } + + { + let multiple_without_factor = "en-US, zh-CN, ja-JP"; + let result = + parse_navigator_language(multiple_without_factor).expect("Should parse language"); + + assert_eq!( + Some("en-US".to_string()), + result, + "en-US language should be matched" + ); + } + + // Malformed + // We don't have any validation, so we should get back the same value + { + let malformed = "ab-CDE-ffff l -- adsfasd '"; + let result = parse_navigator_language(malformed).expect("Should parse language"); + + assert_eq!( + Some(malformed.to_string()), + result, + "The malformed language should be matched" + ); + } + } + + #[test] + fn test_size_from_type() { + let legacy = [ + (Size::new(300, 250), "legacy_300x250"), + (Size::new(250, 250), "legacy_250x250"), + (Size::new(240, 400), "legacy_240x400"), + (Size::new(336, 280), "legacy_336x280"), + (Size::new(180, 150), "legacy_180x150"), + (Size::new(300, 100), "legacy_300x100"), + (Size::new(720, 300), "legacy_720x300"), + (Size::new(468, 60), "legacy_468x60"), + (Size::new(234, 60), "legacy_234x60"), + (Size::new(88, 31), "legacy_88x31"), + (Size::new(120, 90), "legacy_120x90"), + (Size::new(120, 60), "legacy_120x60"), + (Size::new(120, 240), "legacy_120x240"), + (Size::new(125, 125), "legacy_125x125"), + (Size::new(728, 90), "legacy_728x90"), + (Size::new(160, 600), "legacy_160x600"), + (Size::new(120, 600), "legacy_120x600"), + (Size::new(300, 600), "legacy_300x600"), + ]; + + for (expected, type_string) in legacy { + let actual_size = size_from_type(type_string).expect("Should parse Ad type"); + pretty_assertions::assert_eq!( + expected, + actual_size, + "Expected Size does not match with the parsed one" + ); + } + } +} diff --git a/adview-manager/serve/templates/base.html b/adview-manager/serve/templates/base.html index ec13a962c..6847295ed 100644 --- a/adview-manager/serve/templates/base.html +++ b/adview-manager/serve/templates/base.html @@ -17,6 +17,19 @@ display: flex; } + .flex-container { + display: flex; + flex-wrap: wrap; + } + + .flex-6 { + flex: 50%; + } + + .flex-12 { + flex: 100%; + } + .h-100 { height: 100%; } diff --git a/adview-manager/serve/templates/index.html b/adview-manager/serve/templates/index.html index d7753cab7..e5ee5dd91 100644 --- a/adview-manager/serve/templates/index.html +++ b/adview-manager/serve/templates/index.html @@ -7,6 +7,7 @@

AdEx AdView Manager by Ambire

Preview the capabilities and usage of the AdView Manager

diff --git a/adview-manager/serve/templates/next_ad.html b/adview-manager/serve/templates/next_ad.html new file mode 100644 index 000000000..1866d7d8a --- /dev/null +++ b/adview-manager/serve/templates/next_ad.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block title %}Serve an Ad{% endblock %} + +{% block content %} +
+
+
+ {% if next_ad_unit %} + {{ next_ad_unit.html | safe }} + {% else %} +

No matched AdUnit

+ {% endif %} +
+
+
+
+ {% if next_ad_unit %} + + {{ next_ad_unit | json_encode(pretty=true) }} + + {% else %} + No matched AdUnit + {% endif %} +
+
+ + {{ ad_slot | json_encode(pretty=true) }} + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/adview-manager/serve/templates/preview_form.html b/adview-manager/serve/templates/preview_form.html new file mode 100644 index 000000000..2a666ebd6 --- /dev/null +++ b/adview-manager/serve/templates/preview_form.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}Preview an Ad form{% endblock %} + +{% block content %} +
+
+
+
Options
+
+ + +
+
+ + +
+
+ + +
+ {# We only create 2 validator urls for now! #} +
Validators (ordering matters - first validator has priority over second)
+ {% for i in [0, 1, 2, 3, 4] %} +
+ + +
+ {% endfor %} + +
Whitelisted token addresses
+ {% for i in [0, 1, 2, 3, 4] %} +
+ + +
+ {% endfor %} + +
+ + +
+
+ +
+
+
+ {#
+
+ {% if next_ad_unit %} + {{ next_ad_unit.html | safe }} + {% else %} +

No matched AdUnit

+ {% endif %} +
+
#} +
+{% endblock %} \ No newline at end of file diff --git a/adview-manager/src/helpers.rs b/adview-manager/src/helpers.rs index 5e0103d4b..f2bc7e8b5 100644 --- a/adview-manager/src/helpers.rs +++ b/adview-manager/src/helpers.rs @@ -156,7 +156,7 @@ pub fn get_unit_html_with_events( let body = serde_json::to_string(&events_body).expect("It should always serialize EventBody"); - // TODO: check whether the JSON body with `''` quotes executes correctly! + // Be careful with JSON body and always use `'` quotes. let fetch_opts = format!("var fetchOpts = {{ method: 'POST', headers: {{ 'content-type': 'application/json' }}, body: {body} }};"); let validators: String = validators @@ -361,11 +361,9 @@ mod test { .flat_map(|chain| chain.tokens.values().map(|token| token.address)) .collect::>(); - let market_url = ApiUrl::parse("https://market.adex.network").expect("should parse"); let validator_1_url = ApiUrl::parse("https://tom.adex.network").expect("should parse"); let validator_2_url = ApiUrl::parse("https://jerry.adex.network").expect("should parse"); let options = Options { - market_url, market_slot: DUMMY_IPFS[0], publisher_addr: *PUBLISHER, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared diff --git a/adview-manager/src/manager.rs b/adview-manager/src/manager.rs index 188ffa452..45a6b07f9 100644 --- a/adview-manager/src/manager.rs +++ b/adview-manager/src/manager.rs @@ -12,7 +12,6 @@ use async_std::{sync::RwLock, task::block_on}; use chrono::{DateTime, Duration, Utc}; use log::error; use once_cell::sync::Lazy; -use rand::Rng; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use std::{ @@ -63,12 +62,11 @@ pub enum Error { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Options { - #[serde(rename = "marketURL")] - pub market_url: ApiUrl, pub market_slot: IPFS, pub publisher_addr: Address, /// All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared pub whitelisted_tokens: HashSet
, + /// Optional size to be set on the generated HTML for serving the ad. pub size: Option, pub navigator_language: Option, /// Whether or not to disable Video ads. @@ -99,7 +97,7 @@ impl Size { } /// The next [`AdUnit`] to be shown -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NextAdUnit { pub unit: AdUnit, pub price: UnifiedNum, @@ -119,9 +117,13 @@ pub struct StickyAdUnit { /// History entry of impressions (won auctions) which the [`Manager`] holds. #[derive(Debug, Clone)] pub struct HistoryEntry { + /// The time when this auction was won and the ad was shown. pub time: DateTime, + /// The `AdUnit` shown pub unit_id: IPFS, + // The `Campaign` for which this `AdUnit` was show. pub campaign_id: CampaignId, + // The `AdSlot` for which the `AdUnit` was show. pub slot_id: IPFS, } @@ -257,7 +259,14 @@ impl Manager { .map_err(|_| Error::InvalidValidatorUrl)?; // Ordering of the campaigns matters so we will just push them to the first result // We reuse `targeting_input_base`, `accepted_referrers` and `fallback_unit` - let mut first_res: Response = self.client.get(url.as_str()).send().await?.json().await?; + let mut first_res: Response = self + .client + .get(url.as_str()) + .send() + .await? + .error_for_status()? + .json() + .await?; for validator in self.options.validators.iter().skip(1) { let url = validator @@ -306,9 +315,10 @@ impl Manager { } // If two or more units result in the same price, apply random selection between them: this is why we need the seed - let mut rng = rand::thread_rng(); + // let mut rng = rand::thread_rng(); + // let random: f64 = rng.gen::() * (0x80000000_u64 as f64 - 1.0); + let random: f64 = rand::random::() * (0x80000000_u64 as f64 - 1.0); - let random: f64 = rng.gen::() * (0x80000000_u64 as f64 - 1.0); let seed = BigNum::from(random as u64); // Apply targeting, now with adView.* variables, and sort the resulting ad units @@ -433,12 +443,13 @@ mod test { use super::*; use crate::manager::input::Input; use adex_primitives::{ - config::GANACHE_CONFIG, sentry::{ units_for_slot::response::{AdUnit, UnitsWithPrice}, CLICK, }, - test_util::{CAMPAIGNS, DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, PUBLISHER}, + test_util::{ + CAMPAIGNS, DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, PUBLISHER, WHITELISTED_TOKENS, + }, unified_num::FromWhole, }; use wiremock::{ @@ -446,24 +457,19 @@ mod test { Mock, MockServer, ResponseTemplate, }; - fn setup_manager(uri: String) -> Manager { - let market_url = uri.parse().unwrap(); - let whitelisted_tokens = GANACHE_CONFIG - .chains - .values() - .flat_map(|chain| chain.tokens.values().map(|token| token.address)) - .collect::>(); - - let validator_1_url = ApiUrl::parse(&format!("{}/validator-1", uri)).expect("should parse"); - let validator_2_url = ApiUrl::parse(&format!("{}/validator-2", uri)).expect("should parse"); - let validator_3_url = ApiUrl::parse(&format!("{}/validator-3", uri)).expect("should parse"); + fn setup_manager(mock_url: ApiUrl) -> Manager { + let validator_1_url = + ApiUrl::parse(&format!("{mock_url}validator-1")).expect("should parse"); + let validator_2_url = + ApiUrl::parse(&format!("{mock_url}validator-2")).expect("should parse"); + let validator_3_url = + ApiUrl::parse(&format!("{mock_url}validator-3")).expect("should parse"); let options = Options { - market_url, market_slot: DUMMY_IPFS[0], publisher_addr: *PUBLISHER, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared - whitelisted_tokens, + whitelisted_tokens: WHITELISTED_TOKENS.clone(), size: Some(Size::new(300, 100)), navigator_language: Some("bg".into()), disabled_video: false, @@ -595,7 +601,7 @@ mod test { .await; // 2. Set up a manager - let manager = setup_manager(server.uri()); + let manager = setup_manager(server.uri().parse().unwrap()); let res = manager .get_units_for_slot_resp() @@ -609,7 +615,7 @@ mod test { #[tokio::test] async fn check_if_campaign_is_sticky() { - let mut manager = setup_manager("http://localhost:1337".to_string()); + let mut manager = setup_manager("http://localhost:8000".parse().unwrap()); // Case 1 - options has disabled sticky { @@ -617,7 +623,7 @@ mod test { assert!(!manager.is_campaign_sticky(DUMMY_CAMPAIGN.id).await); manager.options.disabled_sticky = false; } - // Case 2 - time is past stickiness treshold, less than 4 minutes ago + // Case 2 - time is past stickiness threshold, less than 4 minutes ago { let history = vec![HistoryEntry { time: Utc::now() - Duration::days(1), // 24 hours ago @@ -650,7 +656,7 @@ mod test { #[tokio::test] async fn check_sticky_ad_unit() { let server = MockServer::start().await; - let mut manager = setup_manager(server.uri()); + let mut manager = setup_manager(server.uri().parse().unwrap()); let history = vec![HistoryEntry { time: Utc::now(), unit_id: DUMMY_AD_UNITS[0].ipfs, @@ -667,9 +673,7 @@ mod test { price: UnifiedNum::from_whole(0.0001), }], }; - let res = manager - .get_sticky_ad_unit(&[campaign], "http://localhost:1337") - .await; + let res = manager.get_sticky_ad_unit(&[campaign], "localhost").await; assert!(res.is_some()); @@ -682,9 +686,7 @@ mod test { }], }; - let res = manager - .get_sticky_ad_unit(&[campaign], "http://localhost:1337") - .await; + let res = manager.get_sticky_ad_unit(&[campaign], "localhost").await; assert!(res.is_none()); } diff --git a/docs/config/ganache.toml b/docs/config/ganache.toml index 2f32729f7..4fd23f00f 100644 --- a/docs/config/ganache.toml +++ b/docs/config/ganache.toml @@ -51,7 +51,7 @@ admins = [ [platform] # This should be changed for tests and use the wiremock url -url = "https://platform.adex.network" +url = "http://localhost:8004" # 20 minutes in milliseconds keep_alive_interval = 1200000 diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 4363ccc87..d3de91888 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -23,6 +23,10 @@ test-util = [] [[example]] name = "accounting_response" +[[example]] +name = "ad_slot" +required-features = ["test-util"] + [[example]] name = "all_spenders_response" diff --git a/primitives/examples/ad_slot.rs b/primitives/examples/ad_slot.rs new file mode 100644 index 000000000..dff87faf0 --- /dev/null +++ b/primitives/examples/ad_slot.rs @@ -0,0 +1,48 @@ +use chrono::{TimeZone, Utc}; +use primitives::{ + targeting::Rules, + test_util::{DUMMY_AD_UNITS, DUMMY_IPFS, IDS, PUBLISHER}, + AdSlot, +}; +use serde_json::{from_value, json}; + +fn main() { + let json = json!({ + "ipfs": "QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR", + "type": "legacy_300x100", + "minPerImpression": null, + "rules": [], + "fallbackUnit": "Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f", + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + // milliseconds + "created": 1564372800000_u64, + "title": "Test AdSlot", + "description": null, + "website": "https://adex.network", + "archived": false, + // milliseconds + "modified": 1564372800000_u64 + }); + + let fallback_unit = DUMMY_AD_UNITS[0].ipfs; + + let expected_ad_slot = AdSlot { + ipfs: DUMMY_IPFS[0], + ad_type: "legacy_300x100".to_string(), + min_per_impression: None, + rules: Rules::default(), + fallback_unit: Some(fallback_unit), + owner: IDS[&PUBLISHER], + created: Utc.ymd(2019, 7, 29).and_hms(4, 0, 0), + title: Some("Test AdSlot".to_string()), + description: None, + website: Some("https://adex.network".to_string()), + archived: false, + modified: Some(Utc.ymd(2019, 7, 29).and_hms(4, 0, 0)), + }; + + pretty_assertions::assert_eq!( + from_value::(json).expect("Should deserialize"), + expected_ad_slot + ); +} diff --git a/primitives/examples/get_cfg_response.rs b/primitives/examples/get_cfg_response.rs index e9cdbce35..42697408f 100644 --- a/primitives/examples/get_cfg_response.rs +++ b/primitives/examples/get_cfg_response.rs @@ -65,7 +65,7 @@ fn main() { } }, "platform": { - "url": "https://platform.adex.network/", + "url": "http://localhost:8004", "keep_alive_interval": 1200000 }, "limits": { diff --git a/primitives/src/ad_slot.rs b/primitives/src/ad_slot.rs index bd569e965..68574fdec 100644 --- a/primitives/src/ad_slot.rs +++ b/primitives/src/ad_slot.rs @@ -54,6 +54,6 @@ pub struct AdSlot { #[serde(default)] pub archived: bool, /// UTC timestamp in milliseconds, changed every time modifiable property is changed - #[serde(with = "ts_milliseconds_option")] + #[serde(default, with = "ts_milliseconds_option")] pub modified: Option>, } diff --git a/primitives/src/platform.rs b/primitives/src/platform.rs index 393830b8b..4e2961711 100644 --- a/primitives/src/platform.rs +++ b/primitives/src/platform.rs @@ -4,6 +4,7 @@ use url::Url; use crate::{AdSlot, AdUnit}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Website { #[serde(default)] pub categories: Vec, diff --git a/primitives/src/targeting/eval.rs b/primitives/src/targeting/eval.rs index c507c1fb9..98cf0be5a 100644 --- a/primitives/src/targeting/eval.rs +++ b/primitives/src/targeting/eval.rs @@ -205,7 +205,6 @@ impl From for SerdeValue { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -// TODO: https://github.com/AdExNetwork/adex-validator-stack-rust/issues/296 pub enum Function { /// Multiplies first two values and then divides product by third value MulDiv(Box, Box, Box), diff --git a/primitives/src/targeting/input.rs b/primitives/src/targeting/input.rs index abd2e9ea2..54812b90a 100644 --- a/primitives/src/targeting/input.rs +++ b/primitives/src/targeting/input.rs @@ -144,7 +144,6 @@ impl GetField for AdView { #[serde(rename_all = "camelCase")] /// Global scope, accessible everywhere pub struct Global { - /// We still use `String`, because the `Event`s have an `Option`al `AdSlot` value. pub ad_slot_id: IPFS, pub ad_slot_type: String, pub publisher_id: Address, diff --git a/primitives/src/test_util.rs b/primitives/src/test_util.rs index f76a455a9..65b11481c 100644 --- a/primitives/src/test_util.rs +++ b/primitives/src/test_util.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, ops::Deref}; +use std::{ + collections::{HashMap, HashSet}, + ops::Deref, +}; use chrono::{TimeZone, Utc}; use once_cell::sync::Lazy; @@ -228,7 +231,7 @@ pub static DUMMY_AD_UNITS: Lazy<[AdUnit; 4]> = Lazy::new(|| { target_url: "https://www.adex.network/?stremio-test-banner-1".to_string(), archived: false, description: Some("Dummy AdUnit description 1".to_string()), - ad_type: "legacy_250x250".to_string(), + ad_type: "legacy_300x100".to_string(), /// Timestamp: 1 564 383 600 created: Utc.ymd(2019, 7, 29).and_hms(9, 0, 0), min_targeting_score: None, @@ -500,3 +503,13 @@ pub static CAMPAIGNS: Lazy<[ChainOf; 3]> = Lazy::new(|| { [campaign_1337_1, campaign_1337_2, campaign_1_1] }); + +/// All the configured tokens in the `ganache.toml` config file +/// in all chains. +pub static WHITELISTED_TOKENS: Lazy> = Lazy::new(|| { + GANACHE_CONFIG + .chains + .values() + .flat_map(|chain| chain.tokens.values().map(|token| token.address)) + .collect() +}); diff --git a/sentry/src/application.rs b/sentry/src/application.rs index 8dda33467..fc6ec8a26 100644 --- a/sentry/src/application.rs +++ b/sentry/src/application.rs @@ -146,7 +146,10 @@ where ]) // allow requests from any origin // "*" - .allow_origin(tower_http::cors::Any); + .allow_origin(tower_http::cors::Any) + // todo: Check whether we should support only the `content-type` + // instead of `Access-Control-Allow-Headers: *` + .allow_headers(tower_http::cors::Any); let router = Router::new() .nest("/channel", channels_router::()) diff --git a/sentry/src/routes/routers.rs b/sentry/src/routes/routers.rs index 565bfa076..6d3c878f7 100644 --- a/sentry/src/routes/routers.rs +++ b/sentry/src/routes/routers.rs @@ -176,7 +176,7 @@ pub fn campaigns_router() -> Router { /// `/v5/units-for-slot` router pub fn units_for_slot_router() -> Router { - Router::new().route("/", get(get_units_for_slot::)) + Router::new().route("/:slot", get(get_units_for_slot::)) } /// `/v5/analytics` router diff --git a/test_harness/platform/Cargo.toml b/test_harness/platform/Cargo.toml new file mode 100644 index 000000000..1a0dbc150 --- /dev/null +++ b/test_harness/platform/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "platform" +authors = ["Ambire ", "Lachezar Lechev "] +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# Domain +adex_primitives = { version = "0.2.0", path = "../../primitives", package = "primitives", features = ["test-util"] } + +# Application errors +anyhow = "1" + +# Async runtime +tokio = { version = "1", features = ["macros", "time", "rt-multi-thread"] } + +# Web Server +axum = {version = "0.5", features = ["headers", "macros"]} + +# (De)Serialization +serde = { version = "^1.0", features = ["derive"] } + +# In-memory database +dashmap = "5" + +# Tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +pretty_assertions = "1" diff --git a/test_harness/platform/src/main.rs b/test_harness/platform/src/main.rs new file mode 100644 index 000000000..e4c8cc73e --- /dev/null +++ b/test_harness/platform/src/main.rs @@ -0,0 +1,57 @@ +use std::{net::SocketAddr, sync::Arc}; + +use adex_primitives::{platform::AdSlotResponse, IPFS}; +use axum::{ + extract::Path, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Extension, Json, Router, Server, +}; +use dashmap::DashMap; +use tracing::info; + +pub type MockedResponses = DashMap; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let tracing_subscriber = tracing_subscriber::FmtSubscriber::new(); + tracing::subscriber::set_global_default(tracing_subscriber) + .expect("setting tracing default failed"); + + let slot_responses = Arc::new(MockedResponses::new()); + + // build our application with a single router + let app = Router::new() + .route("/slot", post(mock_slot_response)) + .route("/slot/:ipfs", get(get_slot)) + .layer(Extension(slot_responses)); + + let socket_addr: SocketAddr = ([127, 0, 0, 1], 8004).into(); + info!("Server running on: {socket_addr}"); + + Server::bind(&socket_addr) + .serve(app.into_make_service()) + .await?; + + Ok(()) +} + +async fn get_slot( + Extension(responses): Extension>, + Path(slot): Path, +) -> Response { + match responses.get(&slot) { + Some(slot) => (StatusCode::OK, Json(slot.value().clone())).into_response(), + None => (StatusCode::NOT_FOUND, "Slot not found").into_response(), + } +} + +async fn mock_slot_response( + Extension(responses): Extension>, + Json(mocked_response): Json, +) -> impl IntoResponse { + responses.insert(mocked_response.slot.ipfs, mocked_response); + + StatusCode::OK +}