redis-cloud 0.12.0

Redis Cloud REST API client library
Documentation
use redis_cloud::connectivity::transit_gateway::{
    Cidr, CidrStatus, TgwAttachmentRequest, TgwUpdateCidrsRequest, TransitGatewayAttachment,
    TransitGatewayInvitation,
};
use redis_cloud::types::TaskStateUpdate;
use redis_cloud::{CloudClient, CloudError, TransitGatewayHandler};
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 attachment_request() -> TgwAttachmentRequest {
    TgwAttachmentRequest {
        aws_account_id: Some("123456789012".to_string()),
        tgw_id: Some("tgw-123".to_string()),
        cidrs: Some(vec!["10.0.0.0/16".to_string(), "10.1.0.0/16".to_string()]),
    }
}

fn attachment_body() -> Value {
    json!({
        "awsAccountId": "123456789012",
        "tgwId": "tgw-123",
        "cidrs": ["10.0.0.0/16", "10.1.0.0/16"]
    })
}

fn task_body(task_id: &str) -> Value {
    json!({
        "taskId": task_id,
        "commandType": "TGW_OPERATION",
        "status": "processing-in-progress",
        "description": "Transit Gateway 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;
}

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("TGW_OPERATION"));
}

#[tokio::test]
async fn standard_tgw_routes_use_expected_methods_paths_and_bodies() {
    let server = MockServer::start().await;
    let gateways_path = "/subscriptions/123/transitGateways";
    let invitations_path = "/subscriptions/123/transitGateways/invitations";
    let attachment_path = "/subscriptions/123/transitGateways/tgw-123/attachment";

    mount_task(&server, "GET", gateways_path, "get-attachments", None).await;
    mount_task(&server, "GET", invitations_path, "get-invitations", None).await;
    mount_task(
        &server,
        "PUT",
        &format!("{invitations_path}/inv-1/accept"),
        "accept-invitation",
        Some(json!({})),
    )
    .await;
    mount_task(
        &server,
        "PUT",
        &format!("{invitations_path}/inv-2/reject"),
        "reject-invitation",
        Some(json!({})),
    )
    .await;
    mount_task(
        &server,
        "POST",
        attachment_path,
        "create-attachment",
        Some(json!({"tgwId": "tgw-123"})),
    )
    .await;
    mount_task(
        &server,
        "PUT",
        attachment_path,
        "update-attachment",
        Some(attachment_body()),
    )
    .await;
    mount_task(
        &server,
        "DELETE",
        attachment_path,
        "delete-attachment",
        None,
    )
    .await;

    let handler = client(&server).transit_gateway();
    assert_task(
        handler.get_attachments(123).await.unwrap(),
        "get-attachments",
    );
    assert_task(
        handler.get_shared_invitations(123).await.unwrap(),
        "get-invitations",
    );
    assert_task(
        handler
            .accept_resource_share(123, "inv-1".to_string())
            .await
            .unwrap(),
        "accept-invitation",
    );
    assert_task(
        handler
            .reject_resource_share(123, "inv-2".to_string())
            .await
            .unwrap(),
        "reject-invitation",
    );
    assert_task(
        handler
            .create_attachment_with_id(123, "tgw-123")
            .await
            .unwrap(),
        "create-attachment",
    );
    assert_task(
        handler
            .update_attachment_cidrs(123, "tgw-123".to_string(), &attachment_request())
            .await
            .unwrap(),
        "update-attachment",
    );
    assert_task(
        handler
            .delete_attachment(123, "tgw-123".to_string())
            .await
            .unwrap(),
        "delete-attachment",
    );
}

#[tokio::test]
async fn active_active_tgw_routes_include_the_region() {
    let server = MockServer::start().await;
    let gateways_path = "/subscriptions/123/regions/7/transitGateways";
    let invitations_path = "/subscriptions/123/regions/7/transitGateways/invitations";
    let attachment_path = "/subscriptions/123/regions/7/transitGateways/tgw-123/attachment";

    mount_task(&server, "GET", gateways_path, "aa-get-attachments", None).await;
    mount_task(&server, "GET", invitations_path, "aa-get-invitations", None).await;
    mount_task(
        &server,
        "PUT",
        &format!("{invitations_path}/inv-1/accept"),
        "aa-accept-invitation",
        Some(json!({})),
    )
    .await;
    mount_task(
        &server,
        "PUT",
        &format!("{invitations_path}/inv-2/reject"),
        "aa-reject-invitation",
        Some(json!({})),
    )
    .await;
    mount_task(
        &server,
        "POST",
        attachment_path,
        "aa-create-attachment",
        Some(attachment_body()),
    )
    .await;
    mount_task(
        &server,
        "PUT",
        attachment_path,
        "aa-update-attachment",
        Some(attachment_body()),
    )
    .await;
    mount_task(
        &server,
        "DELETE",
        attachment_path,
        "aa-delete-attachment",
        None,
    )
    .await;

    let handler = TransitGatewayHandler::new(client(&server));
    assert_task(
        handler.get_attachments_active_active(123, 7).await.unwrap(),
        "aa-get-attachments",
    );
    assert_task(
        handler
            .get_shared_invitations_active_active(123, 7)
            .await
            .unwrap(),
        "aa-get-invitations",
    );
    assert_task(
        handler
            .accept_resource_share_active_active(123, 7, "inv-1".to_string())
            .await
            .unwrap(),
        "aa-accept-invitation",
    );
    assert_task(
        handler
            .reject_resource_share_active_active(123, 7, "inv-2".to_string())
            .await
            .unwrap(),
        "aa-reject-invitation",
    );
    assert_task(
        handler
            .create_attachment_active_active(123, 7, "tgw-123", &attachment_request())
            .await
            .unwrap(),
        "aa-create-attachment",
    );
    assert_task(
        handler
            .update_attachment_cidrs_active_active(
                123,
                7,
                "tgw-123".to_string(),
                &attachment_request(),
            )
            .await
            .unwrap(),
        "aa-update-attachment",
    );
    assert_task(
        handler
            .delete_attachment_active_active(123, 7, "tgw-123".to_string())
            .await
            .unwrap(),
        "aa-delete-attachment",
    );
}

#[test]
fn tgw_request_and_response_models_round_trip_their_wire_fields() {
    let cidrs_raw = json!({
        "cidrs": [{"cidrAddress": "10.0.0.0/16"}],
        "commandType": "UPDATE_TGW_CIDRS"
    });
    let cidrs: TgwUpdateCidrsRequest = serde_json::from_value(cidrs_raw.clone()).unwrap();
    assert_eq!(
        cidrs
            .cidrs
            .as_ref()
            .and_then(|items| items.first())
            .and_then(|cidr| cidr.cidr_address.as_deref()),
        Some("10.0.0.0/16")
    );
    assert_eq!(serde_json::to_value(cidrs).unwrap(), cidrs_raw);

    let request_raw = attachment_body();
    let request: TgwAttachmentRequest = serde_json::from_value(request_raw.clone()).unwrap();
    assert_eq!(request.aws_account_id.as_deref(), Some("123456789012"));
    assert_eq!(serde_json::to_value(request).unwrap(), request_raw);

    let attachment_raw = json!({
        "id": 42,
        "awsTgwUid": "tgw-123",
        "attachmentUid": "tgw-attach-123",
        "status": "active",
        "attachmentStatus": "available",
        "awsAccountId": "123456789012",
        "cidrs": [{"cidrAddress": "10.0.0.0/16", "status": "active"}]
    });
    let attachment: TransitGatewayAttachment =
        serde_json::from_value(attachment_raw.clone()).unwrap();
    assert_eq!(
        attachment
            .cidrs
            .as_ref()
            .and_then(|items| items.first())
            .and_then(|cidr| cidr.status.as_deref()),
        Some("active")
    );
    assert_eq!(serde_json::to_value(attachment).unwrap(), attachment_raw);

    let invitation_raw = json!({
        "id": 9,
        "name": "redis-share",
        "resourceShareUid": "rs-123",
        "awsAccountId": "123456789012",
        "status": "pending",
        "sharedDate": "2026-08-11T12:00:00Z"
    });
    let invitation: TransitGatewayInvitation =
        serde_json::from_value(invitation_raw.clone()).unwrap();
    assert_eq!(invitation.resource_share_uid.as_deref(), Some("rs-123"));
    assert_eq!(serde_json::to_value(invitation).unwrap(), invitation_raw);

    let cidr = Cidr {
        cidr_address: Some("10.2.0.0/16".to_string()),
    };
    assert_eq!(
        serde_json::to_value(cidr).unwrap(),
        json!({"cidrAddress": "10.2.0.0/16"})
    );
    let cidr_status: CidrStatus =
        serde_json::from_value(json!({"cidrAddress": "10.3.0.0/16", "status": "pending"})).unwrap();
    assert_eq!(cidr_status.status.as_deref(), Some("pending"));
}

#[tokio::test]
async fn tgw_errors_propagate_from_the_client() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/subscriptions/123/transitGateways"))
        .respond_with(ResponseTemplate::new(503).set_body_string("TGW temporarily unavailable"))
        .mount(&server)
        .await;

    let error = client(&server)
        .transit_gateway()
        .get_attachments(123)
        .await
        .unwrap_err();
    match error {
        CloudError::ServiceUnavailable { message } => {
            assert_eq!(message, "TGW temporarily unavailable")
        }
        other => panic!("expected ServiceUnavailable, got {other:?}"),
    }
}