use std::sync::OnceLock;
use std::time::Duration;
use crate::config::{Config, ContainerConfig};
use crate::container::Container;
use crate::error::TestError;
use crate::image::Image;
use crate::port::{Port, PortAccess};
use crate::portpicker;
use crate::probe::{MessageProbe, MessageSource};
use crate::service::Service;
static HTTPMOCK_PUBLIC_PORT: OnceLock<Option<Port>> = OnceLock::new();
const SCHEMA: &str = "http";
pub const HTTPMOCK_IMAGE_NAME: &str = "alexliesenfeld/httpmock";
#[derive(Debug, Clone)]
pub struct HttpMockConfig {
hostname: String,
version: String,
image_name: String,
timeout: Duration,
port: Port,
}
impl HttpMockConfig {
pub fn new() -> Self {
Self {
hostname: "backend".to_string(),
version: "latest".to_string(),
image_name: HTTPMOCK_IMAGE_NAME.to_string(),
timeout: Duration::from_secs(30),
port: 5000,
}
}
}
impl HttpMockConfig {
pub fn hostname(&self) -> &str {
&self.hostname
}
pub fn version(&self) -> &str {
&self.version
}
pub fn image_name(&self) -> &str {
&self.image_name
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn port(&self) -> Port {
self.port
}
pub fn builder() -> HttpMockConfigBuilder {
HttpMockConfigBuilder::new()
}
}
#[derive(Debug, Clone)]
pub struct HttpMockConfigBuilder {
config: HttpMockConfig,
}
impl HttpMockConfigBuilder {
fn new() -> Self {
Self {
config: HttpMockConfig::new(),
}
}
pub fn hostname<T: Into<String>>(self, hostname: T) -> Self {
Self {
config: HttpMockConfig {
hostname: hostname.into(),
..self.config
},
}
}
pub fn version<T: Into<String>>(self, version: T) -> Self {
Self {
config: HttpMockConfig {
version: version.into(),
..self.config
},
}
}
pub fn image_name<T: Into<String>>(self, image_name: T) -> Self {
Self {
config: HttpMockConfig {
image_name: image_name.into(),
..self.config
},
}
}
pub fn port(self, port: Port) -> Self {
Self {
config: HttpMockConfig {
port,
..self.config
},
}
}
pub fn timeout(self, timeout: Duration) -> Self {
Self {
config: HttpMockConfig {
timeout,
..self.config
},
}
}
pub fn build(self) -> HttpMockConfig {
self.config
}
}
impl Default for HttpMockConfig {
fn default() -> Self {
Self::new()
}
}
impl Config for HttpMockConfig {
fn hostname(&self) -> &str {
&self.hostname
}
fn port(&self) -> Port {
self.port
}
fn schema(&self) -> &str {
SCHEMA
}
fn to_container_config(&self) -> Result<ContainerConfig, TestError> {
Ok(ContainerConfig::builder(
self.hostname.clone(),
Image::from_repository(self.image_name()).with_version(self.version()),
)
.ports([PortAccess::published_with_fixed_public_port(
self.port,
HTTPMOCK_PUBLIC_PORT
.get_or_init(portpicker::pick_unused_port)
.to_owned()
.expect("No available ports for HTTP Mock container"),
)])
.env([("HTTPMOCK_PORT", self.port)])
.readiness(
MessageProbe::builder("Listening on")
.timeout(self.timeout)
.source(MessageSource::StdErr)
.build(),
)
.build())
}
}
#[derive(Debug)]
pub struct HttpMock {
socket: String,
address: String,
}
impl HttpMock {
pub fn socket(&self) -> &str {
&self.socket
}
pub fn address(&self) -> &str {
&self.address
}
}
impl Service for HttpMock {
type Config = HttpMockConfig;
fn new(config: &Self::Config, container: &Container) -> Self {
let socket = container
.socket(config.port)
.expect("port should be configured")
.to_string();
let address = format!("{SCHEMA}://{socket}");
Self { socket, address }
}
}