pdk_test/services/
httpmock.rs1use 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
24static HTTPMOCK_PUBLIC_PORT: OnceLock<Option<Port>> = OnceLock::new();
26
27const SCHEMA: &str = "http";
28
29pub const HTTPMOCK_IMAGE_NAME: &str = "alexliesenfeld/httpmock";
31
32#[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 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 pub fn hostname(&self) -> &str {
58 &self.hostname
59 }
60
61 pub fn version(&self) -> &str {
63 &self.version
64 }
65
66 pub fn image_name(&self) -> &str {
68 &self.image_name
69 }
70
71 pub fn timeout(&self) -> Duration {
73 self.timeout
74 }
75
76 pub fn port(&self) -> Port {
78 self.port
79 }
80
81 pub fn builder() -> HttpMockConfigBuilder {
83 HttpMockConfigBuilder::new()
84 }
85}
86
87#[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 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 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 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 pub fn port(self, port: Port) -> Self {
135 Self {
136 config: HttpMockConfig {
137 port,
138 ..self.config
139 },
140 }
141 }
142
143 pub fn timeout(self, timeout: Duration) -> Self {
145 Self {
146 config: HttpMockConfig {
147 timeout,
148 ..self.config
149 },
150 }
151 }
152
153 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#[derive(Debug)]
203pub struct HttpMock {
204 socket: String,
205 address: String,
206}
207
208impl HttpMock {
209 pub fn socket(&self) -> &str {
211 &self.socket
212 }
213
214 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}