use tide::{Request, Response}; mod database; mod frontend; mod indieauth; mod micropub; use crate::indieauth::IndieAuthMiddleware; #[derive(Clone)] pub struct ApplicationState where StorageBackend: database::Storage + Send + Sync + 'static, { token_endpoint: surf::Url, authorization_endpoint: surf::Url, media_endpoint: Option, http_client: surf::Client, storage: StorageBackend, } type App = tide::Server>; static MICROPUB_CLIENT: &[u8] = include_bytes!("./index.html"); fn equip_app(mut app: App) -> App where Storage: database::Storage + Send + Sync + Clone, { app.at("/micropub") .with(IndieAuthMiddleware::new()) .get(micropub::get_handler) .post(micropub::post_handler); // The Micropub client. It'll start small, but could grow into something full-featured! app.at("/micropub/client").get(|_: Request<_>| async move { Ok(Response::builder(200) .body(MICROPUB_CLIENT) .content_type("text/html") .build()) }); app.at("/") .with(frontend::ErrorHandlerMiddleware {}) .get(frontend::mainpage) .post(frontend::onboarding_receiver); app.at("/static/*path") .with(frontend::ErrorHandlerMiddleware {}) .get(frontend::handle_static); app.at("/*path") .with(frontend::ErrorHandlerMiddleware {}) .get(frontend::render_post); app.at("/coffee") .with(frontend::ErrorHandlerMiddleware {}) .get(frontend::coffee); app.at("/health").get(|_| async { Ok("OK") }); app } pub async fn get_app_with_redis( token_endpoint: surf::Url, authorization_endpoint: surf::Url, redis_uri: String, media_endpoint: Option, ) -> App { let app = tide::with_state(ApplicationState { token_endpoint, media_endpoint, authorization_endpoint, storage: database::RedisStorage::new(redis_uri).await.unwrap(), http_client: surf::Client::new(), }); equip_app(app) } #[cfg(test)] pub async fn get_app_with_test_redis( token_endpoint: surf::Url, ) -> ( database::RedisInstance, database::RedisStorage, App, ) { use surf::Url; let redis_instance = database::get_redis_instance().await; let backend = database::RedisStorage::new(redis_instance.uri().to_string()) .await .unwrap(); let app = tide::with_state(ApplicationState { token_endpoint, media_endpoint: None, authorization_endpoint: Url::parse("https://indieauth.com/auth").unwrap(), storage: backend.clone(), http_client: surf::Client::new(), }); return (redis_instance, backend, equip_app(app)); } #[cfg(test)] #[allow(unused_variables)] mod tests { use super::*; use database::Storage; use mockito::mock; use serde_json::json; use tide_testing::TideTestingExt; // Helpers async fn create_app() -> ( database::RedisStorage, App, database::RedisInstance, ) { //get_app_with_memory_for_testing(surf::Url::parse(&*mockito::server_url()).unwrap()).await let (r, b, a) = get_app_with_test_redis(surf::Url::parse(&*mockito::server_url()).unwrap()).await; (b, a, r) } async fn post_json( app: &App, json: serde_json::Value, ) -> surf::Response { let request = app .post("/micropub") .header("Authorization", "Bearer test") .header("Content-Type", "application/json") .body(json); return request.send().await.unwrap(); } #[async_std::test] async fn test_no_posting_to_others_websites() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (db, app, _r) = create_app().await; let response = post_json( &app, json!({ "type": ["h-entry"], "properties": { "content": ["Fake news about Aaron Parecki!"], "uid": ["https://aaronparecki.com/posts/fake-news"] } }), ) .await; assert_eq!(response.status(), 403); let response = post_json( &app, json!({ "type": ["h-entry"], "properties": { "content": ["More fake news about Aaron Parecki!"], "url": ["https://aaronparecki.com/posts/more-fake-news"] } }), ) .await; assert_eq!(response.status(), 403); let response = post_json(&app, json!({ "type": ["h-entry"], "properties": { "content": ["Sneaky advertisement designed to creep into someone else's feed! Buy whatever I'm promoting!"], "channel": ["https://aaronparecki.com/feeds/main"] } })).await; assert_eq!(response.status(), 403); } #[async_std::test] async fn test_successful_authorization() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (db, app, _r) = create_app().await; let response: serde_json::Value = app .get("/micropub?q=config") .header("Authorization", "test") .recv_json() .await .unwrap(); assert!(!response["q"].as_array().unwrap().is_empty()); } #[async_std::test] async fn test_unsuccessful_authorization() { let _m = mock("GET", "/") .with_status(400) .with_header("Content-Type", "application/json") .with_body(r#"{"error":"unauthorized","error_description":"A valid access token is required."}"#) .create(); let (db, app, _r) = create_app().await; let response: surf::Response = app .get("/micropub?q=config") .header("Authorization", "test") .send() .await .unwrap(); assert_eq!(response.status(), 401); } #[async_std::test] async fn test_no_auth_header() { let (db, app, _r) = create_app().await; let request: surf::RequestBuilder = app.get("/micropub?q=config"); let response: surf::Response = request.send().await.unwrap(); assert_eq!(response.status(), 401); } #[async_std::test] async fn test_create_post_form_encoded() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (storage, app, _r) = create_app().await; let request: surf::RequestBuilder = app .post("/micropub") .header("Authorization", "Bearer test") .header("Content-Type", "application/x-www-form-urlencoded") .body("h=entry&content=something%20interesting&category[]=test&category[]=stuff"); let mut response: surf::Response = request.send().await.unwrap(); println!( "{:#}", response.body_json::().await.unwrap() ); assert!(response.status() == 201 || response.status() == 202); let uid = response.header("Location").unwrap().last().to_string(); // Assume the post is in the database at this point. let post = storage.get_post(&uid).await.unwrap().unwrap(); assert_eq!( post["properties"]["content"][0]["html"] .as_str() .unwrap() .trim(), "

something interesting

" ); } #[async_std::test] async fn test_create_post_json() { let _m = mock("GET", "/") .with_status(200) .with_header("Content-Type", "application/json") .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#) .create(); let (storage, app, _r) = create_app().await; let mut response = post_json( &app, json!({ "type": ["h-entry"], "properties": { "content": ["This is content!"] } }), ) .await; println!( "{:#}", response.body_json::().await.unwrap() ); assert!(response.status() == 201 || response.status() == 202); let uid = response.header("Location").unwrap().last().to_string(); // Assume the post is in the database at this point. let post = storage.get_post(&uid).await.unwrap().unwrap(); assert_eq!( post["properties"]["content"][0]["html"] .as_str() .unwrap() .trim(), "

This is content!

" ); let feed = storage .get_post("https://fireburn.ru/feeds/main") .await .unwrap() .unwrap(); assert_eq!(feed["children"].as_array().unwrap().len(), 1); assert_eq!(feed["children"][0].as_str().unwrap(), uid); let first_uid = uid; // Test creation of a second post let mut response = post_json( &app, json!({ "type": ["h-entry"], "properties": { "content": ["#moar content for you!"] } }), ) .await; println!( "{:#}", response.body_json::().await.unwrap() ); assert!(response.status() == 201 || response.status() == 202); let uid = response.header("Location").unwrap().last().to_string(); // Assume the post is in the database at this point. //println!("Keys in database: {:?}", storage.mapping.read().await.keys()); let new_feed = storage .get_post("https://fireburn.ru/feeds/main") .await .unwrap() .unwrap(); println!("{}", new_feed["children"]); assert_eq!(new_feed["children"].as_array().unwrap().len(), 2); assert_eq!(new_feed["children"][0].as_str().unwrap(), uid); assert_eq!(new_feed["children"][1].as_str().unwrap(), first_uid); } }