opencloudmesh 0.2.1

Implementation of the OpenCloudMesh protocol
Documentation
// SPDX-FileCopyrightText: 2026 Matthias Kraus <info@opengeomesh.org>
//
// SPDX-License-Identifier: LGPL-3.0-or-later

mod receive_share;
mod send_share;

pub use receive_share::*;
pub use send_share::*;

/// URI for sending or receiving shares
pub const SHARE_ENDPOINT: &str = "/shares";

#[cfg(test)]
mod tests {
    use std::{collections::{HashMap, HashSet}, str::FromStr};

    use http::Uri;
    use ocm_types::{
        discovery::{Capability, Criterium, Discovery},
        share::NewShare,
    };
    use serde::Serialize;
    use serde_json::Value;

    use crate::{
        common::HttpClient,
        drivers::{
            protocols::{MultiProtocol, Permission, Protocol},
            resources::Resource,
            shares::{InMemoryShareRepo, ReceivedShareRepo},
            users::{InMemoryUserRepo, User, UserRepo},
        },
    };

    use super::*;

    struct TestClient<U: UserRepo, R: ReceivedShareRepo> {
        pub users: U,
        pub received_shares: R,
    }

    impl TestClient<InMemoryUserRepo, InMemoryShareRepo> {
        fn new() -> Self {
            Self {
                users: InMemoryUserRepo::default(),
                received_shares: InMemoryShareRepo::default(),
            }
        }
    }

    #[derive(Debug, Clone, Default, PartialEq, PartialOrd, Serialize)]
    struct TestResource();

    impl Resource for TestResource {
        const RESOURCE_TYPE: &str = "test-resource";

        fn uri(&self) -> &str {
            "example.org/test-resource"
        }

        fn name(&self) -> &str {
            "TestResource"
        }
    }

    #[derive(Debug, Clone)]
    struct TestProtocol(Uri);

    impl Protocol for TestProtocol {
        fn new(endpoint: Uri) -> Self
        where
            Self: Sized,
        {
            Self(endpoint)
        }

        fn share_resource(
            &self,
            _provider_id: &crate::drivers::shares::ProviderId,
            _permissions: &HashSet<Permission>
        ) -> Result<MultiProtocol, &'static str> {
            Ok(MultiProtocol {
                additional_protocols: HashMap::from_iter([(
                    TestProtocol::new(self.endpoint().to_owned())
                        .identifier()
                        .to_string(),
                    Value::Null,
                )]),
                ..Default::default()
            })
        }

        fn endpoint(&self) -> &Uri {
            &self.0
        }

        fn identifier(&self) -> &'static str {
            "test-protocol"
        }

        fn supported_resource_types(&self) -> &[&str] {
            &[TestResource::RESOURCE_TYPE]
        }

        fn resolve_client_properties(
            &self,
            _sending_server: &Discovery,
            new_share: &NewShare,
        ) -> Result<
            crate::drivers::protocols::MultiProtocol,
            crate::drivers::protocols::ProtocolError,
        > {
            Ok(crate::drivers::protocols::MultiProtocol {
                additional_protocols: new_share.protocol.additional_protocols.clone(),
                ..Default::default()
            })
        }
    }

    impl HttpClient for TestClient<InMemoryUserRepo, InMemoryShareRepo> {
        async fn get(&self, _url: &Uri) -> Result<String, String> {
            println!("Sharing tests should not require discovery");
            Err("NOT_FOUND".to_string())
        }

        async fn post(&self, url: &Uri, body: serde_json::Value) -> Result<String, String> {
            match url.to_string().as_str() {
                "https://test-receiver.example.org/shares" => receive_share(
                    // &TestClient::new(),
                    &self.received_shares,
                    &self.users,
                    "test-receiver.example.org",
                    serde_json::from_value(body).unwrap(),
                    vec![Criterium::HttpRequestSignatures],
                    &vec![Box::new(TestProtocol::new(
                        Uri::from_str("/something").unwrap(),
                    ))],
                    &Discovery::default(),
                    Some("test-provider.example.org".into()),
                )
                .await
                .map(|ok| serde_json::to_string(&ok).unwrap())
                .map_err(|err| serde_json::to_string(&err).unwrap()),
                _ => Err("NOT_FOUND".to_string()),
            }
        }

        fn allow_http(&self) -> bool {
            false
        }
    }

    #[tokio::test]
    async fn send_receive_share_with_endpoint_scheme() {
        let discovery = Discovery {
            enabled: true,
            api_version: "1.2.0".to_string(),
            end_point: "https://test-receiver.example.org".to_string(),
            provider: Some("Test Receiver".to_string()),
            capabilities: Some(vec![Capability::ProtocolObject]),
            ..Default::default()
        };
        let http_client = TestClient::new();
        send_receive_share(http_client, &discovery).await;
    }

    #[tokio::test]
    async fn send_receive_share_without_endpoint_scheme() {
        let discovery = Discovery {
            enabled: true,
            api_version: "1.2.0".to_string(),
            end_point: "test-receiver.example.org".to_string(),
            provider: Some("Test Receiver".to_string()),
            capabilities: Some(vec![Capability::ProtocolObject]),
            ..Default::default()
        };
        let http_client = TestClient::new();
        send_receive_share(http_client, &discovery).await;
    }

    async fn send_receive_share(
        http_client: TestClient<InMemoryUserRepo, InMemoryShareRepo>,
        receiving_server: &Discovery,
    ) {
        let test_protocol = TestProtocol::new("/test".parse().unwrap());
        let protocol = test_protocol
            .share_resource(&"test".to_string().into(), &HashSet::new())
            .unwrap();

        let new_share = NewShare {
            share_with: "recipient@test-receiver.example.org".try_into().unwrap(),
            name: "TestResource".to_string(),
            description: None,
            provider_id: "test".to_string(),
            owner: "owner@test-provider.example.org".try_into().unwrap(),
            sender: "owner@test-provider.example.org".try_into().unwrap(),
            owner_display_name: None,
            sender_display_name: None,
            share_type: ocm_types::common::ShareType::User,
            resource_type: "test-resource".to_string(),
            expiration: None,
            protocol: protocol.clone().into(),
        };

        // check if share for non-existing recipient is rejected
        send_share(
            &http_client,
            "test".to_string(),
            "owner@test-provider.example.org".try_into().unwrap(),
            receiving_server,
            "recipient@test-receiver.example.org".try_into().unwrap(),
            &TestResource(),
            protocol.clone(),
        )
        .await
        .expect_err("Share to non-existing recipient should be rejected");

        http_client
            .users
            .insert(User {
                id: "recipient".to_string(),
                name: "Receiving Party".to_string(),
            })
            .await
            .unwrap();
        // check if share for recipient is successfully created
        send_share(
            &http_client,
            "test".to_string(),
            "owner@test-provider.example.org".try_into().unwrap(),
            receiving_server,
            "recipient@test-receiver.example.org".try_into().unwrap(),
            &TestResource(),
            protocol,
        )
        .await
        .expect("Share should have been created");

        // check if received share is stored
        let received_share = http_client
            .received_shares
            .get("test-provider.example.org".into(), "test")
            .await
            .expect("Received share was not stored");
        assert_eq!(new_share, received_share.0);
    }
}