#[cfg(test)]
mod tests {
use async_trait::async_trait;
use mockito::{self, Matcher};
use prost::Message;
use reqwest::header::CONTENT_TYPE;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use vss_client::client::VssClient;
use vss_client::error::VssError;
use vss_client::headers::FixedHeaders;
use vss_client::headers::VssHeaderProvider;
use vss_client::headers::VssHeaderProviderError;
use vss_client::types::{
DeleteObjectRequest, DeleteObjectResponse, ErrorCode, ErrorResponse, GetObjectRequest,
GetObjectResponse, KeyValue, ListKeyVersionsRequest, ListKeyVersionsResponse,
PutObjectRequest, PutObjectResponse,
};
use vss_client::util::retry::{ExponentialBackoffRetryPolicy, RetryPolicy};
const APPLICATION_OCTET_STREAM: &'static str = "application/octet-stream";
const GET_OBJECT_ENDPOINT: &'static str = "/getObject";
const PUT_OBJECT_ENDPOINT: &'static str = "/putObjects";
const DELETE_OBJECT_ENDPOINT: &'static str = "/deleteObject";
const LIST_KEY_VERSIONS_ENDPOINT: &'static str = "/listKeyVersions";
#[tokio::test]
async fn test_get() {
let base_url = mockito::server_url().to_string();
let get_request = GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() };
let mock_response = GetObjectResponse {
value: Some(KeyValue { key: "k1".to_string(), version: 2, value: b"k1v2".to_vec() }),
..Default::default()
};
let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT)
.match_header(CONTENT_TYPE.as_str(), APPLICATION_OCTET_STREAM)
.match_body(get_request.encode_to_vec())
.with_status(200)
.with_body(mock_response.encode_to_vec())
.create();
let client = VssClient::new(base_url, retry_policy());
let actual_result = client.get_object(&get_request).await.unwrap();
let expected_result = &mock_response;
assert_eq!(actual_result, *expected_result);
mock_server.expect(1).assert();
}
#[tokio::test]
async fn test_get_with_headers() {
let base_url = mockito::server_url().to_string();
let get_request = GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() };
let mock_response = GetObjectResponse {
value: Some(KeyValue { key: "k1".to_string(), version: 2, value: b"k1v2".to_vec() }),
..Default::default()
};
let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT)
.match_header(CONTENT_TYPE.as_str(), APPLICATION_OCTET_STREAM)
.match_header("headerkey", "headervalue")
.match_body(get_request.encode_to_vec())
.with_status(200)
.with_body(mock_response.encode_to_vec())
.create();
let header_provider = Arc::new(FixedHeaders::new(HashMap::from([(
"headerkey".to_string(),
"headervalue".to_string(),
)])));
let client = VssClient::new_with_headers(base_url, retry_policy(), header_provider);
let actual_result = client.get_object(&get_request).await.unwrap();
let expected_result = &mock_response;
assert_eq!(actual_result, *expected_result);
mock_server.expect(1).assert();
}
#[tokio::test]
async fn test_put() {
let base_url = mockito::server_url().to_string();
let request = PutObjectRequest {
store_id: "store".to_string(),
global_version: Some(4),
transaction_items: vec![KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}],
delete_items: vec![],
};
let mock_response = PutObjectResponse::default();
let mock_server = mockito::mock("POST", PUT_OBJECT_ENDPOINT)
.match_header(CONTENT_TYPE.as_str(), APPLICATION_OCTET_STREAM)
.match_body(request.encode_to_vec())
.with_status(200)
.with_body(mock_response.encode_to_vec())
.create();
let vss_client = VssClient::new(base_url, retry_policy());
let actual_result = vss_client.put_object(&request).await.unwrap();
let expected_result = &mock_response;
assert_eq!(actual_result, *expected_result);
mock_server.expect(1).assert();
}
#[tokio::test]
async fn test_delete() {
let base_url = mockito::server_url().to_string();
let request = DeleteObjectRequest {
store_id: "store".to_string(),
key_value: Some(KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}),
};
let mock_response = DeleteObjectResponse::default();
let mock_server = mockito::mock("POST", DELETE_OBJECT_ENDPOINT)
.match_header(CONTENT_TYPE.as_str(), APPLICATION_OCTET_STREAM)
.match_body(request.encode_to_vec())
.with_status(200)
.with_body(mock_response.encode_to_vec())
.create();
let vss_client = VssClient::new(base_url, retry_policy());
let actual_result = vss_client.delete_object(&request).await.unwrap();
let expected_result = &mock_response;
assert_eq!(actual_result, *expected_result);
mock_server.expect(1).assert();
}
#[tokio::test]
async fn test_list_key_versions() {
let base_url = mockito::server_url().to_string();
let request = ListKeyVersionsRequest {
store_id: "store".to_string(),
page_size: Some(5),
page_token: None,
key_prefix: Some("k".into()),
};
let mock_response = ListKeyVersionsResponse {
key_versions: vec![
KeyValue { key: "k1".to_string(), version: 3, value: vec![] },
KeyValue { key: "k2".to_string(), version: 1, value: vec![] },
],
global_version: Some(4),
next_page_token: Some("k2".into()),
};
let mock_server = mockito::mock("POST", LIST_KEY_VERSIONS_ENDPOINT)
.match_header(CONTENT_TYPE.as_str(), APPLICATION_OCTET_STREAM)
.match_body(request.encode_to_vec())
.with_status(200)
.with_body(mock_response.encode_to_vec())
.create();
let client = VssClient::new(base_url, retry_policy());
let actual_result = client.list_key_versions(&request).await.unwrap();
let expected_result = &mock_response;
assert_eq!(actual_result, *expected_result);
mock_server.expect(1).assert();
}
#[tokio::test]
async fn test_no_such_key_err_handling() {
let base_url = mockito::server_url();
let vss_client = VssClient::new(base_url, retry_policy());
let error_response = ErrorResponse {
error_code: ErrorCode::NoSuchKeyException.into(),
message: "NoSuchKeyException".to_string(),
};
let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT)
.with_status(409)
.with_body(&error_response.encode_to_vec())
.create();
let get_result = vss_client
.get_object(&GetObjectRequest {
store_id: "store".to_string(),
key: "non_existent_key".to_string(),
})
.await;
assert!(matches!(get_result.unwrap_err(), VssError::NoSuchKeyError { .. }));
mock_server.expect(1).assert();
}
#[tokio::test]
async fn test_get_response_without_value() {
let base_url = mockito::server_url();
let vss_client = VssClient::new(base_url, retry_policy());
let mock_response = GetObjectResponse { value: None, ..Default::default() };
let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT)
.with_status(200)
.with_body(&mock_response.encode_to_vec())
.create();
let get_result = vss_client
.get_object(&GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() })
.await;
assert!(matches!(get_result.unwrap_err(), VssError::InternalServerError { .. }));
mock_server.expect(3).assert();
}
#[tokio::test]
async fn test_invalid_request_err_handling() {
let base_url = mockito::server_url();
let vss_client = VssClient::new(base_url, retry_policy());
let error_response = ErrorResponse {
error_code: ErrorCode::InvalidRequestException.into(),
message: "InvalidRequestException".to_string(),
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(400)
.with_body(&error_response.encode_to_vec())
.create();
let get_result = vss_client
.get_object(&GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() })
.await;
assert!(matches!(get_result.unwrap_err(), VssError::InvalidRequestError { .. }));
let put_result = vss_client
.put_object(&PutObjectRequest {
store_id: "store".to_string(),
global_version: Some(4),
transaction_items: vec![KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}],
delete_items: vec![],
})
.await;
assert!(matches!(put_result.unwrap_err(), VssError::InvalidRequestError { .. }));
let delete_result = vss_client
.delete_object(&DeleteObjectRequest {
store_id: "store".to_string(),
key_value: Some(KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}),
})
.await;
assert!(matches!(delete_result.unwrap_err(), VssError::InvalidRequestError { .. }));
let list_result = vss_client
.list_key_versions(&ListKeyVersionsRequest {
store_id: "store".to_string(),
page_size: Some(5),
page_token: None,
key_prefix: Some("k".into()),
})
.await;
assert!(matches!(list_result.unwrap_err(), VssError::InvalidRequestError { .. }));
mock_server.expect(4).assert();
}
#[tokio::test]
async fn test_auth_err_handling() {
let base_url = mockito::server_url();
let vss_client = VssClient::new(base_url, retry_policy());
let error_response = ErrorResponse {
error_code: ErrorCode::AuthException.into(),
message: "AuthException".to_string(),
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(401)
.with_body(&error_response.encode_to_vec())
.create();
let get_result = vss_client
.get_object(&GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() })
.await;
assert!(matches!(get_result.unwrap_err(), VssError::AuthError { .. }));
let put_result = vss_client
.put_object(&PutObjectRequest {
store_id: "store".to_string(),
global_version: Some(4),
transaction_items: vec![KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}],
delete_items: vec![],
})
.await;
assert!(matches!(put_result.unwrap_err(), VssError::AuthError { .. }));
let delete_result = vss_client
.delete_object(&DeleteObjectRequest {
store_id: "store".to_string(),
key_value: Some(KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}),
})
.await;
assert!(matches!(delete_result.unwrap_err(), VssError::AuthError { .. }));
let list_result = vss_client
.list_key_versions(&ListKeyVersionsRequest {
store_id: "store".to_string(),
page_size: Some(5),
page_token: None,
key_prefix: Some("k".into()),
})
.await;
assert!(matches!(list_result.unwrap_err(), VssError::AuthError { .. }));
mock_server.expect(4).assert();
}
struct FailingHeaderProvider {}
#[async_trait]
impl VssHeaderProvider for FailingHeaderProvider {
async fn get_headers(
&self, _request: &[u8],
) -> Result<HashMap<String, String>, VssHeaderProviderError> {
Err(VssHeaderProviderError::InvalidData { error: "test".to_string() })
}
}
#[tokio::test]
async fn test_header_provider_error() {
let get_request = GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() };
let header_provider = Arc::new(FailingHeaderProvider {});
let client =
VssClient::new_with_headers("notused".to_string(), retry_policy(), header_provider);
let result = client.get_object(&get_request).await;
assert!(matches!(result, Err(VssError::AuthError { .. })));
}
#[tokio::test]
async fn test_conflict_err_handling() {
let base_url = mockito::server_url();
let vss_client = VssClient::new(base_url, retry_policy());
let error_response = ErrorResponse {
error_code: ErrorCode::ConflictException.into(),
message: "ConflictException".to_string(),
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(409)
.with_body(&error_response.encode_to_vec())
.create();
let put_result = vss_client
.put_object(&PutObjectRequest {
store_id: "store".to_string(),
global_version: Some(4),
transaction_items: vec![KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}],
delete_items: vec![],
})
.await;
assert!(matches!(put_result.unwrap_err(), VssError::ConflictError { .. }));
mock_server.expect(1).assert();
}
#[tokio::test]
async fn test_internal_server_err_handling() {
let base_url = mockito::server_url();
let vss_client = VssClient::new(base_url, retry_policy());
let error_response = ErrorResponse {
error_code: ErrorCode::InternalServerException.into(),
message: "InternalServerException".to_string(),
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(500)
.with_body(&error_response.encode_to_vec())
.create();
let get_result = vss_client
.get_object(&GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() })
.await;
assert!(matches!(get_result.unwrap_err(), VssError::InternalServerError { .. }));
let put_result = vss_client
.put_object(&PutObjectRequest {
store_id: "store".to_string(),
global_version: Some(4),
transaction_items: vec![KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}],
delete_items: vec![],
})
.await;
assert!(matches!(put_result.unwrap_err(), VssError::InternalServerError { .. }));
let delete_result = vss_client
.delete_object(&DeleteObjectRequest {
store_id: "store".to_string(),
key_value: Some(KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}),
})
.await;
assert!(matches!(delete_result.unwrap_err(), VssError::InternalServerError { .. }));
let list_result = vss_client
.list_key_versions(&ListKeyVersionsRequest {
store_id: "store".to_string(),
page_size: Some(5),
page_token: None,
key_prefix: Some("k".into()),
})
.await;
assert!(matches!(list_result.unwrap_err(), VssError::InternalServerError { .. }));
mock_server.expect(12).assert();
}
#[tokio::test]
async fn test_internal_err_handling() {
let base_url = mockito::server_url();
let vss_client = VssClient::new(base_url, retry_policy());
let error_response =
ErrorResponse { error_code: 999, message: "UnknownException".to_string() };
let mut _mock_server = mockito::mock("POST", Matcher::Any)
.with_status(999)
.with_body(&error_response.encode_to_vec())
.create();
let get_request = GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() };
let get_result = vss_client.get_object(&get_request).await;
assert!(matches!(get_result.unwrap_err(), VssError::InternalError { .. }));
let put_request = PutObjectRequest {
store_id: "store".to_string(),
global_version: Some(4),
transaction_items: vec![KeyValue {
key: "k1".to_string(),
version: 2,
value: b"k1v3".to_vec(),
}],
delete_items: vec![],
};
let put_result = vss_client.put_object(&put_request).await;
assert!(matches!(put_result.unwrap_err(), VssError::InternalError { .. }));
let list_request = ListKeyVersionsRequest {
store_id: "store".to_string(),
page_size: Some(5),
page_token: None,
key_prefix: Some("k".into()),
};
let list_result = vss_client.list_key_versions(&list_request).await;
assert!(matches!(list_result.unwrap_err(), VssError::InternalError { .. }));
let malformed_error_response = b"malformed";
_mock_server = mockito::mock("POST", Matcher::Any)
.with_status(409)
.with_body(&malformed_error_response)
.create();
let get_malformed_err_response = vss_client.get_object(&get_request).await;
assert!(matches!(get_malformed_err_response.unwrap_err(), VssError::InternalError { .. }));
let put_malformed_err_response = vss_client.put_object(&put_request).await;
assert!(matches!(put_malformed_err_response.unwrap_err(), VssError::InternalError { .. }));
let list_malformed_err_response = vss_client.list_key_versions(&list_request).await;
assert!(matches!(list_malformed_err_response.unwrap_err(), VssError::InternalError { .. }));
drop(_mock_server);
let get_network_err = vss_client.get_object(&get_request).await;
assert!(matches!(get_network_err.unwrap_err(), VssError::InternalError { .. }));
let put_network_err = vss_client.put_object(&put_request).await;
assert!(matches!(put_network_err.unwrap_err(), VssError::InternalError { .. }));
let list_network_err = vss_client.list_key_versions(&list_request).await;
assert!(matches!(list_network_err.unwrap_err(), VssError::InternalError { .. }));
}
fn retry_policy() -> impl RetryPolicy<E = VssError> {
ExponentialBackoffRetryPolicy::new(Duration::from_millis(1))
.with_max_attempts(3)
.skip_retry_on_error(|e| {
matches!(
e,
VssError::NoSuchKeyError(..)
| VssError::InvalidRequestError(..)
| VssError::ConflictError(..)
| VssError::AuthError(..)
)
})
}
}