mod templates; pub use templates::{ErrorPage, MainPage, Template}; mod onboarding; pub use onboarding::OnboardingPage; mod indieauth; pub use indieauth::AuthorizationRequestPage; mod login; pub use login::LoginPage; mod mf2; pub use mf2::{Entry, VCard, Feed, Food, POSTS_PER_PAGE}; #[cfg(test)] mod tests { use faker_rand::en_us::internet::Domain; use faker_rand::lorem::Word; use microformats::types::{Document, Item, PropertyValue, Url}; use serde_json::json; use std::cell::RefCell; use std::rc::Rc; enum PostType { Note, Article, ReplyTo(serde_json::Value), ReplyToLink(String), LikeOf(serde_json::Value), LikeOfLink(String), } fn gen_hcard(domain: &str) -> serde_json::Value { use faker_rand::en_us::names::FirstName; json!({ "type": ["h-card"], "properties": { "name": [rand::random::().to_string()], "photo": [format!("https://{domain}/media/me.png")], "uid": [format!("https://{domain}/")], "url": [format!("https://{domain}/")] } }) } fn gen_random_post(domain: &str, kind: PostType) -> serde_json::Value { use faker_rand::lorem::{Paragraph, Sentence}; fn html(content: Paragraph) -> serde_json::Value { json!({ "html": format!("

{}

", content), "value": content.to_string() }) } let uid = format!( "https://{domain}/posts/{}-{}-{}", rand::random::(), rand::random::(), rand::random::() ); let dt = chrono::offset::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); match kind { PostType::Note => { let content = rand::random::(); json!({ "type": ["h-entry"], "properties": { "content": [html(content)], "published": [dt], "uid": [&uid], "url": [&uid], "author": [gen_hcard(domain)] } }) } PostType::Article => { let content = rand::random::(); let name = rand::random::(); json!({ "type": ["h-entry"], "properties": { "content": [html(content)], "published": [dt], "uid": [&uid], "url": [&uid], "author": [gen_hcard(domain)], "name": [name.to_string()] } }) } PostType::ReplyTo(ctx) => { let content = rand::random::(); json!({ "type": ["h-entry"], "properties": { "content": [html(content)], "published": [dt], "uid": [&uid], "url": [&uid], "author": [gen_hcard(domain)], "in-reply-to": [{ "type": ["h-cite"], "properties": ctx["properties"] }] } }) } PostType::ReplyToLink(link) => { let content = rand::random::(); json!({ "type": ["h-entry"], "properties": { "content": [html(content)], "published": [dt], "uid": [&uid], "url": [&uid], "author": [gen_hcard(domain)], "in-reply-to": [link] } }) } PostType::LikeOf(ctx) => { json!({ "type": ["h-entry"], "properties": { "published": [dt], "author": [gen_hcard(domain)], "uid": [&uid], "url": [&uid], "like-of": [{ "type": ["h-cite"], "properties": ctx["properties"] }] } }) } PostType::LikeOfLink(link) => { json!({ "type": ["h-entry"], "properties": { "published": [dt], "author": [gen_hcard(domain)], "uid": [&uid], "url": [&uid], "like-of": [link] } }) } } } fn check_dt_published(mf2: &serde_json::Value, item: &Rc>) { use microformats::types::temporal::Value as TemporalValue; let _item = item.borrow(); let props = _item.properties.borrow(); assert!(props.contains_key("published")); if let Some(PropertyValue::Temporal(TemporalValue::Timestamp(item))) = props.get("published").and_then(|v| v.first()) { use chrono::{DateTime, FixedOffset, NaiveDateTime}; // Faithfully reconstruct the original datetime // I wonder why not just have an Enum that would // get you either date, time or a datetime, // potentially with an offset? let offset = item.as_offset().unwrap().data; let ndt: NaiveDateTime = item.as_date().unwrap().data .and_time(item.as_time().unwrap().data) // subtract the offset here, since we will add it back - offset; let dt = DateTime::::from_utc(ndt, offset); let expected: DateTime = chrono::DateTime::parse_from_rfc3339( mf2["properties"]["published"][0].as_str().unwrap(), ) .unwrap(); assert_eq!(dt, expected); } else { unreachable!() } } fn check_e_content(mf2: &serde_json::Value, item: &Rc>) { let _item = item.borrow(); let props = _item.properties.borrow(); assert!(props.contains_key("content")); if let Some(PropertyValue::Fragment(content)) = props.get("content").and_then(|v| v.first()) { assert_eq!( content.html, mf2["properties"]["content"][0]["html"].as_str().unwrap() ); } else { unreachable!() } } #[test] #[ignore = "see https://gitlab.com/maxburon/microformats-parser/-/issues/7"] fn test_note() { let mf2 = gen_random_post(&rand::random::().to_string(), PostType::Note); let html = crate::mf2::Entry { post: &mf2 }.to_string(); let url: Url = mf2 .pointer("/properties/uid/0") .and_then(|i| i.as_str()) .and_then(|u| u.parse().ok()) .unwrap(); let parsed: Document = microformats::from_html(&html, url.clone()).unwrap(); if let Some(PropertyValue::Item(item)) = parsed.get_item_by_url(&url) { let _item = item.borrow(); let props = _item.properties.borrow(); check_e_content(&mf2, &item); check_dt_published(&mf2, &item); assert!(props.contains_key("uid")); assert!(props.contains_key("url")); assert!(props .get("url") .unwrap() .iter() .any(|i| i == props.get("uid").and_then(|v| v.first()).unwrap())); // XXX: fails because of https://gitlab.com/maxburon/microformats-parser/-/issues/7 assert!(!props.contains_key("name")); } else { unreachable!() } } #[test] fn test_article() { let mf2 = gen_random_post(&rand::random::().to_string(), PostType::Article); let html = crate::mf2::Entry { post: &mf2 }.to_string(); let url: Url = mf2 .pointer("/properties/uid/0") .and_then(|i| i.as_str()) .and_then(|u| u.parse().ok()) .unwrap(); let parsed: Document = microformats::from_html(&html, url.clone()).unwrap(); if let Some(PropertyValue::Item(item)) = parsed.get_item_by_url(&url) { let _item = item.borrow(); let props = _item.properties.borrow(); check_e_content(&mf2, &item); check_dt_published(&mf2, &item); assert!(props.contains_key("uid")); assert!(props.contains_key("url")); assert!(props .get("url") .unwrap() .iter() .any(|i| i == props.get("uid").and_then(|v| v.first()).unwrap())); assert!(props.contains_key("name")); if let Some(PropertyValue::Plain(name)) = props.get("name").and_then(|v| v.first()) { assert_eq!( name, mf2.pointer("/properties/name/0") .and_then(|v| v.as_str()) .unwrap() ); } else { panic!("Name wasn't a plain property!"); } } else { unreachable!() } } #[test] fn test_like_of() { for likeof in [ PostType::LikeOf(gen_random_post( &rand::random::().to_string(), PostType::Note, )), PostType::LikeOfLink(format!( "https://{}/posts/{}-{}-{}", &rand::random::(), &rand::random::(), &rand::random::(), &rand::random::(), )), ] { let mf2 = gen_random_post(&rand::random::().to_string(), likeof); let url: Url = mf2 .pointer("/properties/uid/0") .and_then(|i| i.as_str()) .and_then(|u| u.parse().ok()) .unwrap(); let html = crate::mf2::Entry { post: &mf2 }.to_string(); let parsed: Document = microformats::from_html(&html, url.clone()).unwrap(); if let Some(item) = parsed.items.get(0) { let _item = item.borrow(); let props = _item.properties.borrow(); check_dt_published(&mf2, item); assert!(props.contains_key("like-of")); match props.get("like-of").and_then(|v| v.first()) { Some(PropertyValue::Url(url)) => { assert_eq!( url, &mf2.pointer("/properties/like-of/0") .and_then(|i| i.as_str()) .or_else(|| mf2 .pointer("/properties/like-of/0/properties/uid/0") .and_then(|i| i.as_str())) .and_then(|u| u.parse::().ok()) .unwrap() ); } Some(PropertyValue::Item(_cite)) => { todo!() } other => panic!("Unexpected value in like-of: {:?}", other), } } else { unreachable!() } } } }