redis-cloud 0.12.0

Redis Cloud REST API client library
Documentation
use redis_cloud::connectivity::psc::{
    GcpCreationScript, GcpDeletionScript, PrivateServiceConnectEndpoints,
    PrivateServiceConnectService, PscEndpointUpdateRequest,
};
use redis_cloud::types::TaskStateUpdate;
use redis_cloud::{CloudClient, CloudError, PscHandler};
use serde_json::{Value, json};
use wiremock::matchers::{body_json, header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn client(server: &MockServer) -> CloudClient {
    CloudClient::builder()
        .api_key("test-key")
        .api_secret("test-secret")
        .base_url(server.uri())
        .build()
        .expect("test client should build")
}

fn endpoint_request() -> PscEndpointUpdateRequest {
    PscEndpointUpdateRequest {
        subscription_id: 123,
        psc_service_id: 456,
        endpoint_id: 789,
        gcp_project_id: Some("project-id".to_string()),
        gcp_vpc_name: Some("application-vpc".to_string()),
        gcp_vpc_subnet_name: Some("application-subnet".to_string()),
        endpoint_connection_name: Some("redis-endpoint".to_string()),
    }
}

fn endpoint_body() -> Value {
    json!({
        "subscriptionId": 123,
        "pscServiceId": 456,
        "endpointId": 789,
        "gcpProjectId": "project-id",
        "gcpVpcName": "application-vpc",
        "gcpVpcSubnetName": "application-subnet",
        "endpointConnectionName": "redis-endpoint"
    })
}

fn task_body(task_id: &str) -> Value {
    json!({
        "taskId": task_id,
        "commandType": "PSC_OPERATION",
        "status": "processing-in-progress",
        "description": "PSC operation"
    })
}

async fn mount_task(
    server: &MockServer,
    verb: &str,
    request_path: &str,
    task_id: &str,
    body: Option<Value>,
) {
    let mock = Mock::given(method(verb))
        .and(path(request_path))
        .and(header("x-api-key", "test-key"))
        .and(header("x-api-secret-key", "test-secret"));
    let mock = if let Some(body) = body {
        mock.and(body_json(body))
    } else {
        mock
    };
    mock.respond_with(ResponseTemplate::new(202).set_body_json(task_body(task_id)))
        .expect(1)
        .mount(server)
        .await;
}

async fn mount_script(server: &MockServer, request_path: &str, script: &str) {
    Mock::given(method("GET"))
        .and(path(request_path))
        .and(header("x-api-key", "test-key"))
        .and(header("x-api-secret-key", "test-secret"))
        .respond_with(ResponseTemplate::new(200).set_body_json(script))
        .expect(1)
        .mount(server)
        .await;
}

fn assert_task(task: TaskStateUpdate, expected_id: &str) {
    assert_eq!(task.task_id.as_deref(), Some(expected_id));
    assert_eq!(task.command_type.as_deref(), Some("PSC_OPERATION"));
}

#[tokio::test]
async fn standard_psc_routes_use_expected_methods_paths_and_bodies() {
    let server = MockServer::start().await;
    let service_path = "/subscriptions/123/private-service-connect";
    let endpoints_path = "/subscriptions/123/private-service-connect/456";
    let endpoint_path = "/subscriptions/123/private-service-connect/456/endpoints/789";

    mount_task(&server, "GET", service_path, "get-service", None).await;
    mount_task(
        &server,
        "POST",
        service_path,
        "create-service",
        Some(json!({})),
    )
    .await;
    mount_task(&server, "DELETE", service_path, "delete-service", None).await;
    mount_task(
        &server,
        "POST",
        endpoints_path,
        "create-endpoint",
        Some(endpoint_body()),
    )
    .await;
    mount_task(&server, "GET", endpoints_path, "get-endpoints", None).await;
    mount_task(
        &server,
        "PUT",
        endpoint_path,
        "update-endpoint",
        Some(endpoint_body()),
    )
    .await;
    mount_task(&server, "DELETE", endpoint_path, "delete-endpoint", None).await;
    mount_script(
        &server,
        &format!("{endpoint_path}/creationScripts"),
        "create script",
    )
    .await;
    mount_script(
        &server,
        &format!("{endpoint_path}/deletionScripts"),
        "delete script",
    )
    .await;

    let handler = client(&server).psc();
    assert_task(handler.get_service(123).await.unwrap(), "get-service");
    assert_task(handler.create_service(123).await.unwrap(), "create-service");
    assert_task(handler.delete_service(123).await.unwrap(), "delete-service");
    assert_task(
        handler
            .create_endpoint(123, 456, &endpoint_request())
            .await
            .unwrap(),
        "create-endpoint",
    );
    assert_task(
        handler.get_endpoints(123, 456).await.unwrap(),
        "get-endpoints",
    );
    assert_task(
        handler
            .update_endpoint(123, 456, 789, &endpoint_request())
            .await
            .unwrap(),
        "update-endpoint",
    );
    assert_task(
        handler.delete_endpoint(123, 456, 789).await.unwrap(),
        "delete-endpoint",
    );
    assert_eq!(
        handler
            .get_endpoint_creation_script(123, 456, 789)
            .await
            .unwrap(),
        "create script"
    );
    assert_eq!(
        handler
            .get_endpoint_deletion_script(123, 456, 789)
            .await
            .unwrap(),
        "delete script"
    );
}

#[tokio::test]
async fn active_active_psc_routes_include_the_region() {
    let server = MockServer::start().await;
    let service_path = "/subscriptions/123/regions/7/private-service-connect";
    let endpoints_path = "/subscriptions/123/regions/7/private-service-connect/456";
    let endpoint_path = "/subscriptions/123/regions/7/private-service-connect/456/endpoints/789";

    mount_task(&server, "GET", service_path, "aa-get-service", None).await;
    mount_task(
        &server,
        "POST",
        service_path,
        "aa-create-service",
        Some(json!({})),
    )
    .await;
    mount_task(&server, "DELETE", service_path, "aa-delete-service", None).await;
    mount_task(
        &server,
        "POST",
        endpoints_path,
        "aa-create-endpoint",
        Some(endpoint_body()),
    )
    .await;
    mount_task(&server, "GET", endpoints_path, "aa-get-endpoints", None).await;
    mount_task(
        &server,
        "PUT",
        endpoint_path,
        "aa-update-endpoint",
        Some(endpoint_body()),
    )
    .await;
    mount_task(&server, "DELETE", endpoint_path, "aa-delete-endpoint", None).await;
    mount_script(
        &server,
        &format!("{endpoint_path}/creationScripts"),
        "aa create script",
    )
    .await;
    mount_script(
        &server,
        &format!("{endpoint_path}/deletionScripts"),
        "aa delete script",
    )
    .await;

    let handler = PscHandler::new(client(&server));
    assert_task(
        handler.get_service_active_active(123, 7).await.unwrap(),
        "aa-get-service",
    );
    assert_task(
        handler.create_service_active_active(123, 7).await.unwrap(),
        "aa-create-service",
    );
    assert_task(
        handler.delete_service_active_active(123, 7).await.unwrap(),
        "aa-delete-service",
    );
    assert_task(
        handler
            .create_endpoint_active_active(123, 7, 456, &endpoint_request())
            .await
            .unwrap(),
        "aa-create-endpoint",
    );
    assert_task(
        handler
            .get_endpoints_active_active(123, 7, 456)
            .await
            .unwrap(),
        "aa-get-endpoints",
    );
    assert_task(
        handler
            .update_endpoint_active_active(123, 7, 456, 789, &endpoint_request())
            .await
            .unwrap(),
        "aa-update-endpoint",
    );
    assert_task(
        handler
            .delete_endpoint_active_active(123, 7, 456, 789)
            .await
            .unwrap(),
        "aa-delete-endpoint",
    );
    assert_eq!(
        handler
            .get_endpoint_creation_script_active_active(123, 7, 456, 789)
            .await
            .unwrap(),
        "aa create script"
    );
    assert_eq!(
        handler
            .get_endpoint_deletion_script_active_active(123, 7, 456, 789)
            .await
            .unwrap(),
        "aa delete script"
    );
}

#[test]
fn psc_response_models_round_trip_their_wire_fields() {
    let service_raw = json!({
        "id": 456,
        "connectionHostName": "psc.example.com",
        "serviceAttachmentName": "projects/p/regions/r/serviceAttachments/a",
        "status": "active"
    });
    let service: PrivateServiceConnectService =
        serde_json::from_value(service_raw.clone()).unwrap();
    assert_eq!(service.id, Some(456));
    assert_eq!(serde_json::to_value(service).unwrap(), service_raw);

    let endpoints_raw = json!({
        "pscServiceId": 456,
        "endpoints": [{
            "id": 789,
            "gcpProjectId": "project-id",
            "gcpVpcName": "application-vpc",
            "gcpVpcSubnetName": "application-subnet",
            "endpointConnectionName": "redis-endpoint",
            "status": "active"
        }]
    });
    let endpoints: PrivateServiceConnectEndpoints =
        serde_json::from_value(endpoints_raw.clone()).unwrap();
    assert_eq!(endpoints.endpoints.as_ref().map(Vec::len), Some(1));
    assert_eq!(serde_json::to_value(endpoints).unwrap(), endpoints_raw);

    let creation_raw = json!({
        "bash": "gcloud create",
        "powershell": "gcloud create",
        "terraformGcp": {
            "serviceAttachments": [{
                "name": "attachment",
                "dnsRecord": "redis.example.com",
                "ipAddressName": "redis-ip",
                "forwardingRuleName": "redis-forwarding-rule"
            }]
        }
    });
    let creation: GcpCreationScript = serde_json::from_value(creation_raw.clone()).unwrap();
    assert_eq!(
        creation
            .terraform_gcp
            .as_ref()
            .and_then(|terraform| terraform.service_attachments.as_ref())
            .map(Vec::len),
        Some(1)
    );
    assert_eq!(serde_json::to_value(creation).unwrap(), creation_raw);

    let deletion_raw = json!({"bash": "gcloud delete", "powershell": "gcloud delete"});
    let deletion: GcpDeletionScript = serde_json::from_value(deletion_raw.clone()).unwrap();
    assert_eq!(deletion.bash.as_deref(), Some("gcloud delete"));
    assert_eq!(serde_json::to_value(deletion).unwrap(), deletion_raw);
}

#[tokio::test]
async fn psc_errors_propagate_from_the_client() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/subscriptions/123/private-service-connect"))
        .respond_with(ResponseTemplate::new(404).set_body_string("PSC service missing"))
        .mount(&server)
        .await;

    let error = client(&server).psc().get_service(123).await.unwrap_err();
    match error {
        CloudError::NotFound { message } => assert_eq!(message, "PSC service missing"),
        other => panic!("expected NotFound, got {other:?}"),
    }
}