Content-Length: 20684 | pFad | http://github.com/postgresml/postgresml/pull/1528.diff

thub.com diff --git a/pgml-dashboard/Cargo.lock b/pgml-dashboard/Cargo.lock index 59e710ba5..e14050a5b 100644 --- a/pgml-dashboard/Cargo.lock +++ b/pgml-dashboard/Cargo.lock @@ -1924,16 +1924,6 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" -[[package]] -name = "libloading" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libm" version = "0.2.8" @@ -2223,47 +2213,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "neon" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373" -dependencies = [ - "neon-build", - "neon-macros", - "neon-runtime", - "semver 0.9.0", - "smallvec", -] - -[[package]] -name = "neon-build" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811" - -[[package]] -name = "neon-macros" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf" -dependencies = [ - "quote", - "syn 1.0.109", - "syn-mid", -] - -[[package]] -name = "neon-runtime" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca" -dependencies = [ - "cfg-if", - "libloading", - "smallvec", -] - [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -2586,7 +2535,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pgml" -version = "1.0.4" +version = "1.1.0" dependencies = [ "anyhow", "async-trait", @@ -2605,7 +2554,6 @@ dependencies = [ "parking_lot", "regex", "reqwest", - "rust_bridge", "sea-query", "sea-query-binder", "serde", @@ -3308,31 +3256,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "rust_bridge" -version = "0.1.0" -dependencies = [ - "rust_bridge_macros", - "rust_bridge_traits", -] - -[[package]] -name = "rust_bridge_macros" -version = "0.1.0" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.32", -] - -[[package]] -name = "rust_bridge_traits" -version = "0.1.0" -dependencies = [ - "neon", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3351,7 +3274,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.18", + "semver", ] [[package]] @@ -3616,27 +3539,12 @@ dependencies = [ "smallvec", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "sentry" version = "0.31.5" @@ -4332,17 +4240,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn-mid" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "sync_wrapper" version = "0.1.2" diff --git a/pgml-dashboard/src/lib.rs b/pgml-dashboard/src/lib.rs index 6b5efceb2..3bdc0ecdd 100644 --- a/pgml-dashboard/src/lib.rs +++ b/pgml-dashboard/src/lib.rs @@ -390,7 +390,6 @@ pub fn replace_banner_product( context: &Cluster, ) -> Result { let mut all_notification_cookies = Notifications::get_viewed(cookies); - let current_notification_cookie = all_notification_cookies.iter().position(|x| x.id == id); match current_notification_cookie { @@ -408,8 +407,8 @@ pub fn replace_banner_product( Notifications::update_viewed(&all_notification_cookies, cookies); - // Get the notification that triggered this call. - // Guaranteed to exist since it built the component that called this, so this is safe to unwrap. + // Get the notification that triggered this call.. + // unwrap notifications if fine since we should panic if this is missing. let last_notification = context .notifications .as_ref() @@ -424,11 +423,15 @@ pub fn replace_banner_product( .into_iter() .filter(|n: &Notification| -> bool { let n = n.clone().set_viewed(n.id == id); - Notification::product_filter( - &n, - last_notification.clone().unwrap().level.clone(), - deployment_id.clone(), - ) + if last_notification.clone().is_none() { + return false; + } else { + Notification::product_filter( + &n, + last_notification.clone().unwrap().level.clone(), + deployment_id.clone(), + ) + } }) .next(), _ => None, @@ -519,3 +522,188 @@ pub fn routes() -> Vec { pub async fn migrate(pool: &PgPool) -> anyhow::Result<()> { Ok(sqlx::migrate!("./migrations").run(pool).await?) } + +#[cfg(test)] +mod test { + use super::*; + use crate::components::sections::footers::MarketingFooter; + use crate::guards::Cluster; + use rocket::fairing::AdHoc; + use rocket::http::Cookie; + use rocket::local::asynchronous::Client; + + #[sqlx::test] + async fn test_remove_modal() { + let rocket = rocket::build().mount("/", routes()); + let client = Client::untracked(rocket).await.unwrap(); + + let cookie = vec![ + NotificationCookie { + id: "1".to_string(), + time_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), + time_modal_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), + }, + NotificationCookie { + id: "2".to_string(), + time_viewed: None, + time_modal_viewed: None, + }, + ]; + + let response = client + .get("/notifications/product/modal/remove_modal?id=1") + .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) + .dispatch() + .await; + + let time_modal_viewed = Notifications::get_viewed(response.cookies()) + .get(0) + .unwrap() + .time_modal_viewed; + + // Update modal view time for existing notification cookie + assert_eq!(time_modal_viewed.is_some(), true); + + let response = client + .get("/notifications/product/modal/remove_modal?id=3") + .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) + .dispatch() + .await; + + let time_modal_viewed = Notifications::get_viewed(response.cookies()) + .get(0) + .unwrap() + .time_modal_viewed; + + // Update modal view time for new notification cookie + assert_eq!(time_modal_viewed.is_some(), true); + } + + #[sqlx::test] + async fn test_remove_banner_product() { + let rocket = rocket::build().mount("/", routes()); + let client = Client::untracked(rocket).await.unwrap(); + + let cookie = vec![ + NotificationCookie { + id: "1".to_string(), + time_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), + time_modal_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), + }, + NotificationCookie { + id: "2".to_string(), + time_viewed: None, + time_modal_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), + }, + ]; + + let response = client + .get("/notifications/product/remove_banner?id=1&target=ajskghjfbs") + .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) + .dispatch() + .await; + + let time_viewed = Notifications::get_viewed(response.cookies()) + .get(0) + .unwrap() + .time_viewed; + + // Update view time for existing notification cookie + assert_eq!(time_viewed.is_some(), true); + + let response = client + .get("/notifications/product/remove_banner?id=3&target=ajfadghs") + .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) + .dispatch() + .await; + + let time_viewed = Notifications::get_viewed(response.cookies()) + .get(0) + .unwrap() + .time_viewed; + + // Update view time for new notification cookie + assert_eq!(time_viewed.is_some(), true); + } + + #[sqlx::test] + async fn test_replace_banner_product() { + let notification1 = Notification::new("Test notification 1") + .set_level(&NotificationLevel::ProductMedium) + .set_deployment("1"); + let notification2 = Notification::new("Test notification 2") + .set_level(&NotificationLevel::ProductMedium) + .set_deployment("1"); + let _notification3 = Notification::new("Test notification 3") + .set_level(&NotificationLevel::ProductMedium) + .set_deployment("2"); + let _notification4 = Notification::new("Test notification 4").set_level(&NotificationLevel::ProductMedium); + let _notification5 = Notification::new("Test notification 5").set_level(&NotificationLevel::ProductMarketing); + + let rocket = rocket::build() + .attach(AdHoc::on_request("request", |req, _| { + Box::pin(async { + req.local_cache(|| Cluster { + pool: None, + context: Context { + user: models::User::default(), + cluster: models::Cluster::default(), + dropdown_nav: StaticNav { links: vec![] }, + product_left_nav: StaticNav { links: vec![] }, + marketing_footer: MarketingFooter::new().render_once().unwrap(), + head_items: None, + }, + notifications: Some(vec![ + Notification::new("Test notification 1") + .set_level(&NotificationLevel::ProductMedium) + .set_deployment("1"), + Notification::new("Test notification 2") + .set_level(&NotificationLevel::ProductMedium) + .set_deployment("1"), + Notification::new("Test notification 3") + .set_level(&NotificationLevel::ProductMedium) + .set_deployment("2"), + Notification::new("Test notification 4").set_level(&NotificationLevel::ProductMedium), + Notification::new("Test notification 5").set_level(&NotificationLevel::ProductMarketing), + ]), + }); + }) + })) + .mount("/", routes()); + + let client = Client::tracked(rocket).await.unwrap(); + + let response = client + .get(format!( + "/notifications/product/replace_banner?id={}&deployment_id=1", + notification1.id + )) + .dispatch() + .await; + + let body = response.into_string().await.unwrap(); + let rsp_contains_next_notification = body.contains("Test notification 2"); + + // Ensure the banner is replaced with next notification of same type + assert_eq!(rsp_contains_next_notification, true); + + let response = client + .get(format!( + "/notifications/product/replace_banner?id={}&deployment_id=1", + notification2.id + )) + .dispatch() + .await; + + let body = response.into_string().await.unwrap(); + let rsp_contains_next_notification_3 = body.contains("Test notification 3"); + let rsp_contains_next_notification_4 = body.contains("Test notification 4"); + let rsp_contains_next_notification_5 = body.contains("Test notification 5"); + + // Ensure the next notification is not found since none match deployment id or level + assert_eq!( + rsp_contains_next_notification_3 && rsp_contains_next_notification_4 && rsp_contains_next_notification_5, + false + ); + } +} diff --git a/pgml-dashboard/src/main.rs b/pgml-dashboard/src/main.rs index a2e4fb90c..b31f488e4 100644 --- a/pgml-dashboard/src/main.rs +++ b/pgml-dashboard/src/main.rs @@ -137,7 +137,7 @@ mod test { async fn rocket() -> Rocket { dotenv::dotenv().ok(); - pgml_dashboard::migrate(Cluster::default(None).pool()).await.unwrap(); + pgml_dashboard::migrate(Cluster::default().pool()).await.unwrap(); let mut site_search = markdown::SiteSearch::new() .await diff --git a/pgml-dashboard/src/utils/cookies.rs b/pgml-dashboard/src/utils/cookies.rs index e7e5be1d5..370e3d4b1 100644 --- a/pgml-dashboard/src/utils/cookies.rs +++ b/pgml-dashboard/src/utils/cookies.rs @@ -2,55 +2,123 @@ use chrono; use rocket::http::{Cookie, CookieJar}; use rocket::serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)] pub struct NotificationCookie { pub id: String, pub time_viewed: Option>, pub time_modal_viewed: Option>, } -impl std::fmt::Display for NotificationCookie { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut rsp = format!(r#"{{"id": "{}""#, self.id.clone()); - if self.time_viewed.is_some() { - rsp.push_str(&format!(r#", "time_viewed": "{}""#, self.time_viewed.clone().unwrap())); - } - if self.time_modal_viewed.is_some() { - rsp.push_str(&format!( - r#", "time_modal_viewed": "{}""#, - self.time_modal_viewed.clone().unwrap() - )); - } - rsp.push_str("}"); - return write!(f, "{}", rsp); - } -} - pub struct Notifications {} impl Notifications { - pub fn update_viewed(new: &Vec, cookies: &CookieJar<'_>) { - let serialized = new.iter().map(|x| x.to_string()).collect::>(); + pub fn update_viewed(all_desired_notifications: &Vec, cookies: &CookieJar<'_>) { + let session = Notifications::safe_serialize_session(all_desired_notifications); - let mut cookie = Cookie::new("session", format!(r#"{{"notifications": [{}]}}"#, serialized.join(","))); + let mut cookie = Cookie::new("session", session); cookie.set_max_age(::time::Duration::weeks(4)); cookies.add_private(cookie); } pub fn get_viewed(cookies: &CookieJar<'_>) -> Vec { - let viewed: Vec = match cookies.get_private("session") { - Some(session) => { - match serde_json::from_str::(session.value()).unwrap()["notifications"].as_array() { - Some(items) => items - .into_iter() - .map(|x| serde_json::from_str::(&x.to_string()).unwrap()) - .collect::>(), - _ => vec![], - } - } + match cookies.get_private("session") { + Some(session) => Notifications::safe_deserialize_session(session.value()), None => vec![], - }; + } + } + + pub fn safe_deserialize_session(session: &str) -> Vec { + match serde_json::from_str::(session).unwrap_or_else(|_| { + serde_json::from_str::(&Notifications::safe_serialize_session(&vec![])).unwrap() + })["notifications"] + .as_array() + { + Some(items) => items + .into_iter() + .map(|notification| { + serde_json::from_str::(¬ification.to_string()).unwrap_or_else(|_| { + serde_json::from_str::(¬ification.to_string()) + .and_then(|id| { + Ok(NotificationCookie { + id, + time_viewed: None, + time_modal_viewed: None, + }) + }) + .unwrap_or_else(|_| NotificationCookie::default()) + }) + }) + .collect::>(), + _ => vec![], + } + } + + pub fn safe_serialize_session(cookies: &Vec) -> String { + let serialized = cookies + .iter() + .map(|x| serde_json::to_string(x)) + .filter(|x| x.is_ok()) + .map(|x| x.unwrap()) + .collect::>(); + + format!(r#"{{"notifications": [{}]}}"#, serialized.join(",")) + } +} + +#[cfg(test)] +mod test { + use super::*; + + // Test that we can safely deserialize expected session data. + #[test] + fn test_safe_deserialize_session() { + let session = r#"{"notifications": [{"id": "1", "time_viewed": null, "time_modal_viewed": null}, {"id": "1234567891234", "time_viewed": "2021-08-01T00:00:00Z"}]}"#; + let expected = vec![ + NotificationCookie { + id: "1".to_string(), + time_viewed: None, + time_modal_viewed: None, + }, + NotificationCookie { + id: "1234567891234".to_string(), + time_viewed: Some( + chrono::DateTime::parse_from_rfc3339("2021-08-01T00:00:00Z") + .unwrap() + .into(), + ), + time_modal_viewed: None, + }, + ]; + assert_eq!(Notifications::safe_deserialize_session(session), expected); + } + + // Test that new notification system is backwards compatible. + #[test] + fn test_safe_deserialize_session_old_form() { + let session = r#"{"notifications": ["123456789"]}"#; + let expected = vec![NotificationCookie { + id: "123456789".to_string(), + time_viewed: None, + time_modal_viewed: None, + }]; + assert_eq!(Notifications::safe_deserialize_session(session), expected); + } + + #[test] + fn test_safe_deserialize_session_empty() { + let session = r#"{}"#; + let expected: Vec = vec![]; + assert_eq!(Notifications::safe_deserialize_session(session), expected); + } - viewed + #[test] + fn test_safe_serialize_session() { + let cookies = vec![NotificationCookie { + id: "1".to_string(), + time_viewed: None, + time_modal_viewed: None, + }]; + let expected = r#"{"notifications": [{"id":"1","time_viewed":null,"time_modal_viewed":null}]}"#; + assert_eq!(Notifications::safe_serialize_session(&cookies), expected); } }








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/postgresml/postgresml/pull/1528.diff

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy