indieweb 0.9.2

A collection of utilities for working with the IndieWeb.
Documentation
#![cfg(test)]
use microformats::types::{Class, KnownClass};

use super::Action;
use crate::standards::{
        indieauth::AccessToken,
        micropub::action::{ActionResponse, CreationProperties},
    };

async fn mock_micropub_request(
    client: impl crate::http::Client,
    endpoint: &url::Url,
    token: &AccessToken,
    action: &Action,
    response: &ActionResponse,
    message: impl ToString,
) {
    assert_eq!(
        action.send(&client, endpoint, token).await,
        Ok(response.to_owned()),
        "crafts expected response for {}",
        message.to_string()
    );
}

#[tracing_test::traced_test]
#[tokio::test]
async fn action_send_request() {
    let mut client = crate::test::Client::new().await;
    let token = AccessToken::new("a-bad-token");

    client
        .mock_server
        .mock("POST", "/action-create-sync")
        .with_header(http::header::CONTENT_TYPE.to_string(), "application/json")
        .with_header(http::header::LOCATION.to_string(), "http://example.com/new")
        .with_status(http::StatusCode::CREATED.as_u16().into())
        .expect(1)
        .create_async()
        .await;

    let u = format!("{}/action-create-sync", client.mock_server.url())
        .parse()
        .unwrap();

    mock_micropub_request(
        client,
        &u,
        &token,
        &Action::Create {
            properties: Box::new(CreationProperties {
                r#type: Class::Known(KnownClass::Entry),
                parameters: Default::default(),
                extra_fields: Default::default(),
            }),
            files: Default::default(),
        },
        &ActionResponse::Created {
            sync: true,
            location: "http://example.com/new".parse().unwrap(),
            rel: Default::default(),
        },
        "create sync",
    )
    .await;
}

#[test]
fn deser_creation_props() {
    let props_json_in_list = serde_json::json!({
        "type": ["h-entry"],
        "properties": {
            "slug": ["wow"],
            "title": "grrr"
        }
    });

    let create_props: CreationProperties =
        serde_json::from_value(props_json_in_list).expect("converted props from json");

    assert_eq!(create_props.parameters.slug, Some("wow".to_string()));

    let props_json_in_str = serde_json::json!({
        "type": ["h-entry"],
        "properties": {
            "slug": "wower",
            "title": "grrr"
        }
    });

    let create_props: CreationProperties =
        serde_json::from_value(props_json_in_str).expect("converted props from json");

    assert_eq!(create_props.parameters.slug, Some("wower".to_string()));
}

#[cfg(feature = "experimental_batch")]
mod batch_tests {
    use super::*;
    use crate::standards::micropub::action::{BatchActionResponse, BatchActionResult};

    #[test]
    fn batch_action_serialization() {
        let create_action = Action::Create {
            properties: Box::new(CreationProperties {
                r#type: Class::Known(KnownClass::Entry),
                parameters: Default::default(),
                extra_fields: Default::default(),
            }),
            files: Default::default(),
        };

        let batch = Action::Batch {
            actions: vec![create_action],
        };

        let json = batch.into_json();
        assert_eq!(json["action"], "batch");
        assert!(json["actions"].is_array());
    }

    #[test]
    fn batch_action_deserialization() {
        let json = serde_json::json!({
            "action": "batch",
            "actions": [
                {
                    "type": ["h-entry"],
                    "properties": {
                        "content": ["hello world"]
                    }
                }
            ]
        });

        let action: Action = serde_json::from_value(json).unwrap();
        assert!(matches!(action, Action::Batch { .. }));
    }

    #[test]
    fn batch_response_serialization() {
        let response = BatchActionResponse::new(vec![
            BatchActionResult::success(0, Some("https://example.com/post/1".parse().unwrap())),
            BatchActionResult::failure(1, "Something went wrong".to_string()),
        ]);

        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("\"index\":0"));
        assert!(json.contains("\"success\":true"));
        assert!(json.contains("\"index\":1"));
        assert!(json.contains("\"success\":false"));
    }

    #[test]
    fn batch_response_deserialization() {
        let json = r#"{
            "results": [
                {"index": 0, "success": true, "location": "https://example.com/post/1"},
                {"index": 1, "success": false, "error": "Failed to create post"}
            ]
        }"#;

        let response: BatchActionResponse = serde_json::from_str(json).unwrap();
        assert_eq!(response.results.len(), 2);
        assert!(response.results[0].success);
        assert!(!response.results[1].success);
        assert_eq!(response.success_count(), 1);
        assert_eq!(response.failed_count(), 1);
    }

    #[test]
    fn batch_mixed_actions() {
        let json = serde_json::json!({
            "action": "batch",
            "actions": [
                {
                    "type": ["h-entry"],
                    "properties": {"content": ["first post"]}
                },
                {
                    "action": "delete",
                    "url": "https://example.com/old-post"
                }
            ]
        });

        let action: Action = serde_json::from_value(json).unwrap();
        if let Action::Batch { actions } = action {
            assert_eq!(actions.len(), 2);
            assert!(matches!(actions[0], Action::Create { .. }));
            assert!(matches!(actions[1], Action::Delete(_)));
        } else {
            panic!("Expected Batch action");
        }
    }
}