use std::{collections::VecDeque, sync::Arc};
use azure_core::http::StatusCode;
use futures::future::BoxFuture;
use super::{
PageResult, PartitionRoutingRefresh, PipelineContext, PipelineNode, PipelineNodeState,
RequestExecutor, RequestTarget, ResolvedRange, TopologyProvider,
};
use crate::{
diagnostics::DiagnosticsContextBuilder,
models::{
effective_partition_key::EffectivePartitionKey, AccountReference, ActivityId,
CosmosOperation, CosmosResponse, CosmosResponseHeaders, CosmosStatus, DatabaseReference,
FeedRange, PartitionKey, SubStatusCode,
},
options::DiagnosticsOptions,
};
pub(crate) struct MockLeaf {
pages: VecDeque<crate::error::Result<PageResult>>,
feed_range: Option<FeedRange>,
snapshot: Option<PipelineNodeState>,
}
impl MockLeaf {
pub fn with_pages(pages: Vec<crate::error::Result<PageResult>>) -> Self {
Self {
pages: pages.into(),
feed_range: None,
snapshot: None,
}
}
pub fn with_feed_range(mut self, range: FeedRange) -> Self {
self.feed_range = Some(range);
self
}
pub fn with_snapshot(mut self, state: PipelineNodeState) -> Self {
self.snapshot = Some(state);
self
}
}
#[async_trait::async_trait]
impl PipelineNode for MockLeaf {
async fn next_page(
&mut self,
_context: &mut PipelineContext<'_>,
) -> crate::error::Result<PageResult> {
self.pages
.pop_front()
.expect("MockLeaf: no more page results")
}
#[cfg(test)]
fn into_children(self) -> Vec<Box<dyn PipelineNode>> {
vec![]
}
fn snapshot_state(&self) -> crate::error::Result<PipelineNodeState> {
Ok(self.snapshot.clone().unwrap_or(PipelineNodeState::Drained))
}
fn feed_range(&self) -> Option<&FeedRange> {
self.feed_range.as_ref()
}
fn topology_can_change(&self) -> bool {
false
}
}
pub(crate) struct NoopRequestExecutor;
impl RequestExecutor for NoopRequestExecutor {
fn execute_request<'a>(
&'a mut self,
_operation: &'a CosmosOperation,
_target: RequestTarget,
_partition_routing_refresh: PartitionRoutingRefresh,
_continuation: Option<String>,
) -> BoxFuture<'a, crate::error::Result<CosmosResponse>> {
Box::pin(async {
Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("noop executor should not be called")
.build())
})
}
}
pub(crate) struct MockRequestExecutor {
pub responses: VecDeque<crate::error::Result<CosmosResponse>>,
pub refresh_calls: Vec<PartitionRoutingRefresh>,
pub continuation_calls: Vec<Option<String>>,
}
impl MockRequestExecutor {
pub fn new(responses: Vec<crate::error::Result<CosmosResponse>>) -> Self {
Self {
responses: responses.into(),
refresh_calls: Vec::new(),
continuation_calls: Vec::new(),
}
}
}
impl RequestExecutor for MockRequestExecutor {
fn execute_request<'a>(
&'a mut self,
_operation: &'a CosmosOperation,
_target: RequestTarget,
partition_routing_refresh: PartitionRoutingRefresh,
continuation: Option<String>,
) -> BoxFuture<'a, crate::error::Result<CosmosResponse>> {
self.refresh_calls.push(partition_routing_refresh);
self.continuation_calls.push(continuation);
let response = self.responses.pop_front().expect("mock request response");
Box::pin(async move { response })
}
}
pub(crate) struct NoopTopologyProvider;
impl TopologyProvider for NoopTopologyProvider {
fn resolve_ranges<'a>(
&'a mut self,
_range: &'a FeedRange,
_refresh: PartitionRoutingRefresh,
) -> BoxFuture<'a, crate::error::Result<Vec<ResolvedRange>>> {
Box::pin(async {
Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("noop topology provider should not be called")
.build())
})
}
}
pub(crate) struct MockTopologyProvider {
results: VecDeque<crate::error::Result<Vec<ResolvedRange>>>,
}
impl MockTopologyProvider {
pub fn new(results: Vec<crate::error::Result<Vec<ResolvedRange>>>) -> Self {
Self {
results: results.into(),
}
}
}
impl TopologyProvider for MockTopologyProvider {
fn resolve_ranges<'a>(
&'a mut self,
_range: &'a FeedRange,
_refresh: PartitionRoutingRefresh,
) -> BoxFuture<'a, crate::error::Result<Vec<ResolvedRange>>> {
let result = self
.results
.pop_front()
.expect("MockTopologyProvider: no more results");
Box::pin(async move { result })
}
}
pub(crate) fn unwrap_page(result: crate::error::Result<PageResult>) -> CosmosResponse {
match result.expect("expected Ok result") {
PageResult::Page { response, .. } => response,
PageResult::Drained => panic!("expected Page, got Drained"),
PageResult::SplitRequired { .. } => panic!("expected Page, got SplitRequired"),
}
}
pub(crate) fn assert_drained(result: crate::error::Result<PageResult>) {
match result.expect("expected Ok result") {
PageResult::Drained => {}
PageResult::Page { .. } => panic!("expected Drained, got Page"),
PageResult::SplitRequired { .. } => panic!("expected Drained, got SplitRequired"),
}
}
pub(crate) fn operation() -> CosmosOperation {
let account = AccountReference::with_master_key(
url::Url::parse("https://test.documents.azure.com:443/").unwrap(),
"dGVzdA==",
);
let database = DatabaseReference::from_name(account, "db".to_owned());
CosmosOperation::read_database(database)
}
pub(crate) fn logical_partition_target() -> RequestTarget {
RequestTarget::LogicalPartitionKey(PartitionKey::from("pk"))
}
pub(crate) fn epk_range_target() -> RequestTarget {
let range = FeedRange::new(
EffectivePartitionKey::MIN,
EffectivePartitionKey::from("80"),
)
.unwrap();
RequestTarget::effective_partition_key_range(range.clone(), "0".to_string(), range)
}
pub(crate) fn response(body: &[u8]) -> CosmosResponse {
response_with_continuation(body, None)
}
pub(crate) fn response_with_continuation(
body: &[u8],
continuation: Option<&str>,
) -> CosmosResponse {
let mut diagnostics = DiagnosticsContextBuilder::new(
ActivityId::new_uuid(),
Arc::new(DiagnosticsOptions::default()),
);
diagnostics.set_operation_status(StatusCode::Ok, None);
let mut headers = CosmosResponseHeaders::new();
headers.continuation = continuation.map(str::to_owned);
CosmosResponse::new(
body.to_vec(),
headers,
CosmosStatus::new(StatusCode::Ok),
Arc::new(diagnostics.complete()),
)
}
pub(crate) fn gone_error() -> crate::error::CosmosError {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::InternalServerError,
))
.with_status(CosmosStatus::from_parts(
StatusCode::Gone,
Some(SubStatusCode::PARTITION_KEY_RANGE_GONE),
))
.with_message("partition topology changed")
.with_response_parts(crate::models::CosmosResponsePayload::new(
Vec::new(),
CosmosResponseHeaders::default(),
))
.build()
}
pub(crate) fn non_topology_gone_error() -> crate::error::CosmosError {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::InternalServerError,
))
.with_status(CosmosStatus::from_parts(
StatusCode::Gone,
Some(SubStatusCode::NAME_CACHE_STALE),
))
.with_message("name cache is stale")
.with_response_parts(crate::models::CosmosResponsePayload::new(
Vec::new(),
CosmosResponseHeaders::default(),
))
.build()
}