pdk_test/services/
httpmock.rs

1// Copyright (c) 2025, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! HTTP mock service for integration testing
6//!
7//! This module provides predefined types and configurations for HTTP mock services
8//! in integration tests. It supports creating mock HTTP endpoints for testing
9//! API interactions and responses.
10//!
11
12use std::sync::OnceLock;
13use std::time::Duration;
14
15use crate::config::{Config, ContainerConfig};
16use crate::container::Container;
17use crate::error::TestError;
18use crate::image::Image;
19use crate::port::{Port, PortAccess};
20use crate::portpicker;
21use crate::probe::{MessageProbe, MessageSource};
22use crate::service::Service;
23
24/// Public port for HTTP Mock is chosen once and reused for the whole test suite
25static HTTPMOCK_PUBLIC_PORT: OnceLock<Option<Port>> = OnceLock::new();
26
27const SCHEMA: &str = "http";
28
29/// Default Httpmock Docker image name.
30pub const HTTPMOCK_IMAGE_NAME: &str = "alexliesenfeld/httpmock";
31
32/// Configuration for a Httpmock service.
33#[derive(Debug, Clone)]
34pub struct HttpMockConfig {
35    hostname: String,
36    version: String,
37    image_name: String,
38    timeout: Duration,
39    port: Port,
40}
41
42impl HttpMockConfig {
43    /// Create a new Httpmock configuration.
44    pub fn new() -> Self {
45        Self {
46            hostname: "backend".to_string(),
47            version: "latest".to_string(),
48            image_name: HTTPMOCK_IMAGE_NAME.to_string(),
49            timeout: Duration::from_secs(30),
50            port: 5000,
51        }
52    }
53}
54
55impl HttpMockConfig {
56    /// Returns the Httpmock service hostname.
57    pub fn hostname(&self) -> &str {
58        &self.hostname
59    }
60
61    /// Returns the Httpmock Docker version.
62    pub fn version(&self) -> &str {
63        &self.version
64    }
65
66    /// Returns the Httpmock Docker image name.
67    pub fn image_name(&self) -> &str {
68        &self.image_name
69    }
70
71    /// Returns the Httpmock service readiness timeout.
72    pub fn timeout(&self) -> Duration {
73        self.timeout
74    }
75
76    /// Returns the Httpmock service configuration port.
77    pub fn port(&self) -> Port {
78        self.port
79    }
80
81    /// Returns a builder for [`HttpMockConfig`].
82    pub fn builder() -> HttpMockConfigBuilder {
83        HttpMockConfigBuilder::new()
84    }
85}
86
87/// Builder for [`HttpMockConfig`].
88#[derive(Debug, Clone)]
89pub struct HttpMockConfigBuilder {
90    config: HttpMockConfig,
91}
92
93impl HttpMockConfigBuilder {
94    fn new() -> Self {
95        Self {
96            config: HttpMockConfig::new(),
97        }
98    }
99
100    /// Sets the Httpmock service hostname.
101    /// By default set to "backend".
102    pub fn hostname<T: Into<String>>(self, hostname: T) -> Self {
103        Self {
104            config: HttpMockConfig {
105                hostname: hostname.into(),
106                ..self.config
107            },
108        }
109    }
110
111    /// Sets the Httpmock Docker version.
112    /// By default set to "latest".
113    pub fn version<T: Into<String>>(self, version: T) -> Self {
114        Self {
115            config: HttpMockConfig {
116                version: version.into(),
117                ..self.config
118            },
119        }
120    }
121
122    /// Sets the Httpmock Docker image name.
123    /// By default set to [`HTTPMOCK_IMAGE_NAME`].
124    pub fn image_name<T: Into<String>>(self, image_name: T) -> Self {
125        Self {
126            config: HttpMockConfig {
127                image_name: image_name.into(),
128                ..self.config
129            },
130        }
131    }
132
133    /// Sets the Httpmock configuration port.
134    pub fn port(self, port: Port) -> Self {
135        Self {
136            config: HttpMockConfig {
137                port,
138                ..self.config
139            },
140        }
141    }
142
143    /// Sets the Httpmock service readiness timeout.
144    pub fn timeout(self, timeout: Duration) -> Self {
145        Self {
146            config: HttpMockConfig {
147                timeout,
148                ..self.config
149            },
150        }
151    }
152
153    /// Build the Httpmock configuration.
154    pub fn build(self) -> HttpMockConfig {
155        self.config
156    }
157}
158
159impl Default for HttpMockConfig {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl Config for HttpMockConfig {
166    fn hostname(&self) -> &str {
167        &self.hostname
168    }
169
170    fn port(&self) -> Port {
171        self.port
172    }
173
174    fn schema(&self) -> &str {
175        SCHEMA
176    }
177
178    fn to_container_config(&self) -> Result<ContainerConfig, TestError> {
179        Ok(ContainerConfig::builder(
180            self.hostname.clone(),
181            Image::from_repository(self.image_name()).with_version(self.version()),
182        )
183        .ports([PortAccess::published_with_fixed_public_port(
184            self.port,
185            HTTPMOCK_PUBLIC_PORT
186                .get_or_init(portpicker::pick_unused_port)
187                .to_owned()
188                .expect("No available ports for HTTP Mock container"),
189        )])
190        .env([("HTTPMOCK_PORT", self.port)])
191        .readiness(
192            MessageProbe::builder("Listening on")
193                .timeout(self.timeout)
194                .source(MessageSource::StdErr)
195                .build(),
196        )
197        .build())
198    }
199}
200
201/// Represents an Httpmock service instance.
202#[derive(Debug)]
203pub struct HttpMock {
204    socket: String,
205    address: String,
206}
207
208impl HttpMock {
209    /// Returns the socket for configuring the Httpmock service.
210    pub fn socket(&self) -> &str {
211        &self.socket
212    }
213
214    /// Returns the address of the Httpmock service.
215    pub fn address(&self) -> &str {
216        &self.address
217    }
218}
219
220impl Service for HttpMock {
221    type Config = HttpMockConfig;
222
223    fn new(config: &Self::Config, container: &Container) -> Self {
224        let socket = container
225            .socket(config.port)
226            .expect("port should be configured")
227            .to_string();
228        let address = format!("{SCHEMA}://{socket}");
229        Self { socket, address }
230    }
231}