use ed25519_dalek::{
SigningKey, VerifyingKey,
pkcs8::{EncodePrivateKey, spki::der::pem::LineEnding},
};
use rand::prelude::ThreadRng;
use testcontainers::{
ContainerAsync, CopyDataSource, GenericImage,
core::{ContainerPort, ImageExt, WaitFor, wait::HttpWaitStrategy},
runners::AsyncRunner,
};
use url::{Host, Url};
use crate::{client::Client, error::ContainerError};
#[derive(Debug, Clone)]
pub struct ContainerBuilder {
image_name: String,
image_tag: String,
internal_port: ContainerPort,
api_token: String,
signing_key: Option<SigningKey>,
}
impl Default for ContainerBuilder {
fn default() -> Self {
Self {
image_name: "thenativeweb/eventsourcingdb".to_string(),
image_tag: "latest".to_string(),
internal_port: ContainerPort::Tcp(3000),
api_token: "secret".to_string(),
signing_key: None,
}
}
}
impl ContainerBuilder {
#[must_use]
pub fn with_image_tag(mut self, tag: &str) -> Self {
self.image_tag = tag.to_string();
self
}
#[must_use]
pub fn with_api_token(mut self, token: &str) -> Self {
self.api_token = token.to_string();
self
}
#[must_use]
pub fn with_port(mut self, port: impl Into<ContainerPort>) -> Self {
self.internal_port = port.into();
self
}
#[must_use]
pub fn with_signing_key(mut self) -> Self {
let mut rng: ThreadRng = rand::rng();
self.signing_key = Some(SigningKey::generate(&mut rng));
self
}
pub async fn start(self) -> Result<Container, ContainerError> {
let mut cmd_args = vec![
"run",
"--api-token",
&self.api_token,
"--data-directory-temporary",
"--http-enabled",
"--https-enabled=false",
];
let mut test_container = GenericImage::new(self.image_name, self.image_tag)
.with_exposed_port(self.internal_port)
.with_wait_for(WaitFor::Http(Box::new(
HttpWaitStrategy::new("/api/v1/ping")
.with_port(self.internal_port)
.with_expected_status_code(200u16),
)))
.with_startup_timeout(std::time::Duration::from_secs(10));
if let Some(signing_key) = &self.signing_key {
cmd_args.push("--signing-key-file=/tmp/signing_key.pem");
test_container = test_container.with_copy_to(
"/tmp/signing_key.pem",
CopyDataSource::Data(Vec::from(
signing_key.to_pkcs8_pem(LineEnding::default())?.as_bytes(),
)),
);
}
let instance = test_container.with_cmd(cmd_args).start().await?;
Ok(Container {
internal_port: self.internal_port,
api_token: self.api_token.clone(),
verifying_key: self.signing_key.map(|k| k.verifying_key()),
instance,
})
}
}
#[derive(Debug)]
pub struct Container {
instance: ContainerAsync<GenericImage>,
internal_port: ContainerPort,
api_token: String,
verifying_key: Option<VerifyingKey>,
}
impl Container {
#[must_use]
pub fn builder() -> ContainerBuilder {
ContainerBuilder::default()
}
pub async fn start_default() -> Result<Container, ContainerError> {
Self::builder().start().await
}
pub async fn start_preview() -> Result<Container, ContainerError> {
Self::builder().with_image_tag("preview").start().await
}
pub async fn get_host(&self) -> Result<Host, ContainerError> {
Ok(self.instance.get_host().await?)
}
pub async fn get_mapped_port(&self) -> Result<u16, ContainerError> {
Ok(self.instance.get_host_port_ipv4(self.internal_port).await?)
}
pub async fn get_base_url(&self) -> Result<Url, ContainerError> {
let host = self.get_host().await?;
let port = self.get_mapped_port().await?;
Ok(Url::parse(&format!("http://{host}:{port}"))?)
}
#[must_use]
pub fn get_api_token(&self) -> &str {
self.api_token.as_str()
}
#[must_use]
pub fn get_verifying_key(&self) -> Option<&VerifyingKey> {
self.verifying_key.as_ref()
}
pub async fn stop(self) -> Result<(), ContainerError> {
self.instance.stop().await?;
Ok(())
}
pub async fn get_client(&self) -> Result<Client, ContainerError> {
let base_url = self.get_base_url().await?;
Ok(Client::new(base_url, self.api_token.clone()))
}
}