mdbook_validator/
docker.rs

1//! Docker operations abstraction for testing.
2//!
3//! Provides a trait for Docker exec operations, enabling mocking in tests
4//! to cover error paths (`create_exec` failure, `start_exec` failure, `inspect_exec` failure).
5
6use anyhow::Result;
7
8use crate::error::ValidatorError;
9use async_trait::async_trait;
10use bollard::exec::{CreateExecOptions, CreateExecResults, StartExecOptions, StartExecResults};
11use bollard::service::ExecInspectResponse;
12use bollard::Docker;
13
14/// Trait for Docker exec operations.
15///
16/// Enables mocking in tests to verify error handling without Docker failures.
17/// Uses async-trait for dyn dispatch (Rust async fn in traits doesn't support dyn yet).
18#[async_trait]
19pub trait DockerOperations: Send + Sync {
20    /// Create an exec instance in a container.
21    async fn create_exec(
22        &self,
23        container_id: &str,
24        options: CreateExecOptions<String>,
25    ) -> Result<CreateExecResults>;
26
27    /// Start an exec instance.
28    async fn start_exec(
29        &self,
30        exec_id: &str,
31        options: Option<StartExecOptions>,
32    ) -> Result<StartExecResults>;
33
34    /// Inspect an exec instance to get exit code.
35    async fn inspect_exec(&self, exec_id: &str) -> Result<ExecInspectResponse>;
36}
37
38/// Real implementation wrapping [`bollard::Docker`].
39///
40/// This is the default implementation used in production.
41pub struct BollardDocker {
42    inner: Docker,
43}
44
45impl BollardDocker {
46    /// Create a new `BollardDocker` from a [`bollard::Docker`] instance.
47    #[must_use]
48    pub fn new(docker: Docker) -> Self {
49        Self { inner: docker }
50    }
51}
52
53#[async_trait]
54impl DockerOperations for BollardDocker {
55    async fn create_exec(
56        &self,
57        container_id: &str,
58        options: CreateExecOptions<String>,
59    ) -> Result<CreateExecResults> {
60        self.inner
61            .create_exec(container_id, options)
62            .await
63            .map_err(|e| {
64                ValidatorError::ContainerExec {
65                    message: format!("create_exec failed: {e}"),
66                }
67                .into()
68            })
69    }
70
71    async fn start_exec(
72        &self,
73        exec_id: &str,
74        options: Option<StartExecOptions>,
75    ) -> Result<StartExecResults> {
76        self.inner.start_exec(exec_id, options).await.map_err(|e| {
77            ValidatorError::ContainerExec {
78                message: format!("start_exec failed: {e}"),
79            }
80            .into()
81        })
82    }
83
84    async fn inspect_exec(&self, exec_id: &str) -> Result<ExecInspectResponse> {
85        self.inner.inspect_exec(exec_id).await.map_err(|e| {
86            ValidatorError::ContainerExec {
87                message: format!("inspect_exec failed: {e}"),
88            }
89            .into()
90        })
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
97
98    use super::*;
99
100    #[test]
101    fn test_bollard_docker_new() {
102        // Just verify the type compiles with the trait
103        fn assert_send_sync<T: Send + Sync>() {}
104        assert_send_sync::<BollardDocker>();
105    }
106}