pcs-external 0.3.0

Ppoppo Chat System (PCS) External API client -- gRPC client for the External Developer Platform
Documentation
//! In-process fake of [`crate::PcsExternalClient`] for boundary testing.
//!
//! `MemoryPcsExternal<S>` exposes the same capability-gated methods as
//! `PcsExternalClient<S>` but records calls in-memory and returns canned
//! responses. Use it to test code that calls the SDK without a live PCS
//! endpoint.
//!
//! Enabled by the `test-support` Cargo feature.
//!
//! ## Example
//!
//! ```rust
//! # #[cfg(feature = "test-support")]
//! # {
//! use pcs_external::test_support::MemoryPcsExternal;
//! use pcs_external::scopes::SendOnly;
//! use pcs_external::types::{Ppnum, RecipientList, TemplateId};
//!
//! let fake = MemoryPcsExternal::<SendOnly>::new();
//! // No calls yet — send_calls() is empty.
//! assert_eq!(fake.send_calls().len(), 0);
//! # }
//! ```

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use std::marker::PhantomData;
use std::sync::Mutex;

use crate::error::Error;
use crate::scopes::{GetSendStatusCapable, PcsExternalScopeSet, SendAlertCapable};
use crate::types::{
    PollConfig, RecipientList, SendOutcome, SendRequestId, SendRequestState, SendStatus,
    SendStatusTotals, TemplateId,
};

/// One recorded call to `send_alert`.
#[derive(Debug, Clone)]
pub struct RecordedSend {
    pub template: TemplateId,
    pub recipients: RecipientList,
    pub poll: Option<PollConfig>,
}

#[derive(Debug, Default)]
struct State {
    sends: Vec<RecordedSend>,
    next_send_outcome: Option<Result<SendOutcome, Error>>,
    next_status: Option<Result<SendStatus, Error>>,
}

/// In-memory PCS External client for tests. See module docs.
pub struct MemoryPcsExternal<S: PcsExternalScopeSet> {
    state: Mutex<State>,
    _scope: PhantomData<S>,
}

impl<S: PcsExternalScopeSet> std::fmt::Debug for MemoryPcsExternal<S> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MemoryPcsExternal").finish()
    }
}

impl<S: PcsExternalScopeSet> MemoryPcsExternal<S> {
    #[must_use]
    pub fn new() -> Self {
        Self {
            state: Mutex::new(State::default()),
            _scope: PhantomData,
        }
    }

    /// Inspect all recorded `send_alert` calls.
    pub fn send_calls(&self) -> Vec<RecordedSend> {
        self.state.lock().unwrap().sends.clone()
    }
}

impl<S: SendAlertCapable> MemoryPcsExternal<S> {
    /// Set the response that the next `send_alert` call will return.
    /// If not set, a default `SendOutcome { state: Queued }` is synthesised.
    pub fn with_send_outcome(self, outcome: Result<SendOutcome, Error>) -> Self {
        self.state.lock().unwrap().next_send_outcome = Some(outcome);
        self
    }

    /// Same API surface as [`crate::PcsExternalClient::send_alert`].
    pub async fn send_alert(
        &self,
        template: &TemplateId,
        recipients: &RecipientList,
        poll: Option<&PollConfig>,
    ) -> Result<SendOutcome, Error> {
        let mut state = self.state.lock().unwrap();
        state.sends.push(RecordedSend {
            template: template.clone(),
            recipients: recipients.clone(),
            poll: poll.cloned(),
        });
        state.next_send_outcome.take().unwrap_or_else(|| {
            Ok(SendOutcome {
                id: SendRequestId::new("fake-id"),
                state: SendRequestState::Queued,
                total_recipients: recipients.len() as u32,
            })
        })
    }
}

impl<S: GetSendStatusCapable> MemoryPcsExternal<S> {
    /// Same API surface as [`crate::PcsExternalClient::get_send_status`].
    pub async fn get_send_status(&self, id: &SendRequestId) -> Result<SendStatus, Error> {
        let mut state = self.state.lock().unwrap();
        state.next_status.take().unwrap_or_else(|| {
            Ok(SendStatus {
                id: id.clone(),
                state: SendRequestState::Completed,
                totals: SendStatusTotals::default(),
            })
        })
    }
}