ocpi 0.3.5

Unofficial, in progress, OCPI implementation
Documentation
mod test_store;

use {
    ocpi::{
        types::{self, CiString, CredentialsToken, CsString},
        CredentialsModule,
    },
    serde_json::json,
    wiremock::{matchers, Mock, MockServer, ResponseTemplate},
};

use std::{
    str::FromStr,
    sync::atomic::{AtomicUsize, Ordering},
};

use ocpi::{NoCommandsHandler, Party};
use once_cell::sync::OnceCell;
pub use test_store::TestStore;
use wiremock::http::HeaderValue;

pub fn create_cpo_and_store() -> (ocpi::Cpo<TestStore, NoCommandsHandler>, TestStore) {
    let store = TestStore::default();
    (
        ocpi::Cpo::new(
            "http://localhost:8000".parse().expect("parsing unused url"),
            store.clone(),
            ocpi::Client::default(),
        ),
        store,
    )
}

#[cfg(test)]
#[rustfmt::skip::macros(json)]
async fn create_versions_mock(cpo_token: impl AsRef<str>) -> (types::Url, MockServer) {
    let mock = MockServer::start().await;
    let mock_url = mock.uri().parse::<types::Url>().expect("Mock URI as URL");

    let b64_token = base64::encode(cpo_token.as_ref());
    let authorization_header = format!("Token {}", b64_token);

    Mock::given(matchers::method("GET"))
        .and(matchers::path("/versions"))
        .and(matchers::header(
            "Authorization",
            HeaderValue::from_str(&authorization_header).expect("HeaderValue"),
        ))
        .and(matchers::header_exists("X-Request-Id"))
        .and(matchers::header_exists("X-Correlation-Id"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!(
            {
		"status_code": 1000,
		"status_message": "Success",
		"timestamp": "2015-06-30T21:59:59Z",
		"data":
		[
		    {
			"version": "2.1.1",
			"url": "http://www.server.com/ocpi/2.1.1/"
		    },
		    {
			"version": "2.2",
			"url": format!("{}/2.2", mock.uri())
		    }
		]
            }
        )))
        .mount(&mock)
        .await;

    Mock::given(matchers::method("GET"))
        .and(matchers::path("/2.2"))
        .and(matchers::header(
            "Authorization",
            HeaderValue::from_str(&format!("Token {}", b64_token)).expect("HeaderValue"),
        ))
        .and(matchers::header_exists("X-Request-Id"))
        .and(matchers::header_exists("X-Correlation-Id"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!(
            {
		"status_code": 1000,
		"status_message": "Success",
		"timestamp": "2015-06-30T21:59:59Z",
		"data": {
		    "version": "2.2",
		    "endpoints": [
			{
			    "identifier": "credentials",
			    "role": "SENDER",
			    "url": format!("{}/2.2/credentials", mock.uri())
			}
		    ]
		}
            }
        )))
        .mount(&mock)
        .await;

    (mock_url, mock)
}

fn ctx(credentials_token: CredentialsToken) -> ocpi::Context {
    static COUNTER: OnceCell<AtomicUsize> = OnceCell::new();
    let c = COUNTER.get_or_init(|| AtomicUsize::new(1));
    let req_id = c.fetch_add(1, Ordering::Relaxed);
    let corr_id = c.fetch_add(1, Ordering::Relaxed);

    ocpi::Context {
        request_id: format!("{req_id}"),
        correlation_id: format!("{corr_id}"),
        credentials_token,
    }
}

/// Tests registration by using a mock server as EMP initiator
/// and a test store.
#[tokio::test]
async fn test_registration() {
    // The token we give CPO and expect it to use back.

    // Token used for cpo -> emsp
    let cpo_token = "<i_really_am_a_dummy_token>"
        .parse::<CredentialsToken>()
        .expect("Parsing dummy token");

    let (mock_url, _mock) = create_versions_mock(cpo_token.as_str()).await;

    let (cpo, store) = create_cpo_and_store();
    let reg_token = store.create_reg_token(); // token to use in registration
    let emsp_name: CsString<100> = "TestingEMSPParty".parse().expect("emsp_name");
    let emsp_party_id: CiString<3> = "WUT".parse().expect("emsp_party_id");
    let emsp_country_code: CiString<2> = "se".parse().expect("emsp_country_code");

    let roles = vec![ocpi::types::CredentialsRole {
        role: ocpi::types::Role::Emsp,
        business_details: ocpi::types::BusinessDetails {
            name: emsp_name.clone(),
            website: None,
            logo: None,
        },
        party_id: emsp_party_id.clone(),
        country_code: emsp_country_code.clone(),
    }];

    let cpo_credentials = cpo
        .credentials_post(
            ctx(reg_token),
            ocpi::types::Credential {
                token: cpo_token.clone(),
                url: mock_url.join("/versions").expect("Versions url"),
                roles,
            },
        )
        .await
        .expect("POST credentials");

    let emsp_party = store
        .by_token_we_use(&cpo_token)
        .expect("Expected a party to be created");

    assert_ne!(cpo_credentials.token, cpo_token);
    assert_eq!(cpo_credentials.token, emsp_party.token_they_use());

    assert_eq!(emsp_party.name, emsp_name);
    assert_eq!(emsp_party.roles.len(), 1);
    assert_eq!(emsp_party.roles[0].business_details.name, emsp_name);
    assert_eq!(emsp_party.roles[0].party_id, emsp_party_id);
    assert_eq!(emsp_party.roles[0].country_code, emsp_country_code);
}

/// Tests registration by using a mock server as EMP initiator
/// and a test store.
#[tokio::test]
async fn test_put_registration() {
    // The token we give CPO and expect it to use back.

    // Token used for cpo -> emsp
    let cpo_token = "<i_really_am_a_dummy_token>"
        .parse::<CredentialsToken>()
        .expect("Parsing dummy token");

    let (mock_url, _mock) = create_versions_mock(cpo_token.as_str()).await;

    let (cpo, store) = create_cpo_and_store();
    let reg_token = store.create_reg_token(); // token to use in registration

    let mut roles = vec![ocpi::types::CredentialsRole {
        role: ocpi::types::Role::Emsp,
        business_details: ocpi::types::BusinessDetails {
            name: "TestingEMSPPartyBeforeUpdate".parse().expect("emsp_name"),
            website: None,
            logo: None,
        },
        party_id: "WUT".parse().expect("emsp_party_id"),
        country_code: "se".parse().expect("emsp_country_code"),
    }];

    let cpo_credentials = cpo
        .credentials_post(
            ctx(reg_token),
            ocpi::types::Credential {
                token: cpo_token.clone(),
                url: mock_url.join("/versions").expect("Versions url"),
                roles: roles.clone(),
            },
        )
        .await
        .expect("POST credentials");

    let token_to_send = cpo_credentials.token;

    let new_cpo_token = "<Im the new cpo token use in put>"
        .parse::<CredentialsToken>()
        .expect("Parsing dummy token");

    let (new_mock_url, _mock) = create_versions_mock(new_cpo_token.as_str()).await;
    let new_business_name: CsString<100> =
        "TestingEMSPPartyAfterUpdate".parse().expect("emsp_name");

    roles[0].business_details.name = new_business_name.clone();

    let cpo_credentials = cpo
        .credentials_put(
            ctx(token_to_send.clone()),
            ocpi::types::Credential {
                token: new_cpo_token.clone(),
                url: new_mock_url.join("/versions").expect("Versions url"),
                roles,
            },
        )
        .await
        .expect("PUT credentials");

    let party = store.by_token_we_use(&new_cpo_token).unwrap();

    assert_ne!(party.token_they_use, token_to_send);
    assert_eq!(cpo_credentials.token, party.token_they_use);

    assert_eq!(party.token_we_use, new_cpo_token);
}