Use testcontainers for integration tests
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
pub mod acme;
|
||||
pub mod dkim;
|
||||
pub mod dns;
|
||||
pub mod rfc2136;
|
||||
|
||||
use registry::{
|
||||
schema::{
|
||||
@@ -22,6 +23,7 @@ use crate::utils::server::TestServerBuilder;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn automation_tests() {
|
||||
crate::utils::containers::ensure_acme().await;
|
||||
let mut test = TestServerBuilder::new("automation_tests")
|
||||
.await
|
||||
.with_listener(NetworkListenerProtocol::Http, "http", 8898, false)
|
||||
@@ -116,4 +118,5 @@ async fn automation_tests() {
|
||||
acme::test(&test).await;
|
||||
dkim::test(&test).await;
|
||||
dns::test(&test).await;
|
||||
rfc2136::test(&test).await;
|
||||
}
|
||||
|
||||
179
tests/src/automation/rfc2136.rs
Normal file
179
tests/src/automation/rfc2136.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{net::Ipv4Addr, time::Duration as StdDuration};
|
||||
|
||||
use common::network::dns::update::DnsUpdater;
|
||||
use dns_update::{DnsRecord, DnsRecordType, TLSARecord, TlsaCertUsage, TlsaMatching, TlsaSelector};
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::{IpProtocol, TsigAlgorithm},
|
||||
structs::{DnsServer, DnsServerTsig, SecretKey, SecretKeyValue},
|
||||
},
|
||||
types::duration::Duration,
|
||||
};
|
||||
|
||||
use crate::utils::server::TestServer;
|
||||
|
||||
const ZONE: &str = "stalwart.test";
|
||||
const KEY_B64: &str = "c3RhbHdhcnQtdGVzdC10c2lnLXNlY3JldC1rZXkxMjM0NTY3ODkw";
|
||||
|
||||
pub async fn test(test: &TestServer) {
|
||||
println!("Running RFC2136 (PowerDNS) tests...");
|
||||
crate::utils::containers::ensure_powerdns().await;
|
||||
|
||||
let udp = DnsUpdater::build(dns_server(IpProtocol::Udp), test.server.core.clone())
|
||||
.await
|
||||
.expect("Failed to build UDP RFC2136 updater");
|
||||
|
||||
// Create, replace and delete an A record over UDP (TSIG signed)
|
||||
let a_name = "rfc2136-a.stalwart.test";
|
||||
udp.updater
|
||||
.set_rrset(
|
||||
a_name,
|
||||
DnsRecordType::A,
|
||||
60,
|
||||
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 1))],
|
||||
ZONE,
|
||||
)
|
||||
.await
|
||||
.expect("set A");
|
||||
assert_eq!(
|
||||
list(&udp, a_name, DnsRecordType::A, 1).await,
|
||||
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 1))]
|
||||
);
|
||||
|
||||
udp.updater
|
||||
.set_rrset(
|
||||
a_name,
|
||||
DnsRecordType::A,
|
||||
60,
|
||||
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 2))],
|
||||
ZONE,
|
||||
)
|
||||
.await
|
||||
.expect("replace A");
|
||||
assert_eq!(
|
||||
list(&udp, a_name, DnsRecordType::A, 1).await,
|
||||
vec![DnsRecord::A(Ipv4Addr::new(10, 0, 0, 2))]
|
||||
);
|
||||
|
||||
udp.updater
|
||||
.set_rrset(a_name, DnsRecordType::A, 0, vec![], ZONE)
|
||||
.await
|
||||
.expect("delete A");
|
||||
assert!(list(&udp, a_name, DnsRecordType::A, 0).await.is_empty());
|
||||
|
||||
// Publish two TLSA records at the same owner in a single set_rrset call
|
||||
let tlsa_name = "_25._tcp.rfc2136-tlsa.stalwart.test";
|
||||
let leaf: Vec<u8> = (0..32).collect();
|
||||
let intermediate: Vec<u8> = (32..64).collect();
|
||||
udp.updater
|
||||
.set_rrset(
|
||||
tlsa_name,
|
||||
DnsRecordType::TLSA,
|
||||
60,
|
||||
vec![
|
||||
DnsRecord::TLSA(TLSARecord {
|
||||
cert_usage: TlsaCertUsage::DaneEe,
|
||||
selector: TlsaSelector::Spki,
|
||||
matching: TlsaMatching::Sha256,
|
||||
cert_data: leaf.clone(),
|
||||
}),
|
||||
DnsRecord::TLSA(TLSARecord {
|
||||
cert_usage: TlsaCertUsage::DaneTa,
|
||||
selector: TlsaSelector::Spki,
|
||||
matching: TlsaMatching::Sha256,
|
||||
cert_data: intermediate.clone(),
|
||||
}),
|
||||
],
|
||||
ZONE,
|
||||
)
|
||||
.await
|
||||
.expect("set TLSA");
|
||||
let tlsa = list(&udp, tlsa_name, DnsRecordType::TLSA, 2).await;
|
||||
let cert_datas: Vec<Vec<u8>> = tlsa
|
||||
.iter()
|
||||
.filter_map(|r| match r {
|
||||
DnsRecord::TLSA(t) => Some(t.cert_data.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(cert_datas.contains(&leaf), "leaf TLSA missing: {tlsa:?}");
|
||||
assert!(
|
||||
cert_datas.contains(&intermediate),
|
||||
"intermediate TLSA missing: {tlsa:?}"
|
||||
);
|
||||
udp.updater
|
||||
.set_rrset(tlsa_name, DnsRecordType::TLSA, 0, vec![], ZONE)
|
||||
.await
|
||||
.expect("cleanup TLSA");
|
||||
|
||||
// The TCP transport must also attach the TSIG signer
|
||||
let tcp = DnsUpdater::build(dns_server(IpProtocol::Tcp), test.server.core.clone())
|
||||
.await
|
||||
.expect("Failed to build TCP RFC2136 updater");
|
||||
let txt_name = "rfc2136-txt.stalwart.test";
|
||||
tcp.updater
|
||||
.set_rrset(
|
||||
txt_name,
|
||||
DnsRecordType::TXT,
|
||||
60,
|
||||
vec![DnsRecord::TXT("rfc2136-tcp-signed".to_string())],
|
||||
ZONE,
|
||||
)
|
||||
.await
|
||||
.expect("set TXT over TCP");
|
||||
assert_eq!(
|
||||
list(&tcp, txt_name, DnsRecordType::TXT, 1).await,
|
||||
vec![DnsRecord::TXT("rfc2136-tcp-signed".to_string())]
|
||||
);
|
||||
tcp.updater
|
||||
.set_rrset(txt_name, DnsRecordType::TXT, 0, vec![], ZONE)
|
||||
.await
|
||||
.expect("cleanup TXT");
|
||||
}
|
||||
|
||||
async fn list(
|
||||
updater: &DnsUpdater,
|
||||
name: &str,
|
||||
record_type: DnsRecordType,
|
||||
expected_len: usize,
|
||||
) -> Vec<DnsRecord> {
|
||||
let mut latest = Vec::new();
|
||||
for _ in 0..20 {
|
||||
latest = updater
|
||||
.updater
|
||||
.list_rrset(name, record_type, ZONE)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if latest.len() == expected_len {
|
||||
return latest;
|
||||
}
|
||||
tokio::time::sleep(StdDuration::from_millis(50)).await;
|
||||
}
|
||||
latest
|
||||
}
|
||||
|
||||
fn dns_server(protocol: IpProtocol) -> DnsServer {
|
||||
DnsServer::Tsig(DnsServerTsig {
|
||||
host: "127.0.0.1".parse().unwrap(),
|
||||
port: 5300,
|
||||
key_name: "stalwart-update-key".to_string(),
|
||||
key: SecretKey::Value(SecretKeyValue {
|
||||
secret: KEY_B64.into(),
|
||||
}),
|
||||
protocol,
|
||||
tsig_algorithm: TsigAlgorithm::HmacSha256,
|
||||
description: "Test RFC2136 DNS server".to_string(),
|
||||
member_tenant_id: None,
|
||||
timeout: Duration::from_millis(10_000),
|
||||
ttl: Duration::from_millis(60_000),
|
||||
polling_interval: Duration::from_millis(100),
|
||||
propagation_timeout: Duration::from_millis(10_000),
|
||||
propagation_delay: None,
|
||||
})
|
||||
}
|
||||
@@ -40,15 +40,21 @@ pub async fn cluster_tests() {
|
||||
"COORDINATOR=<coordinator_type> cargo test`"
|
||||
));
|
||||
let coordinator = match coordinator_id.as_str() {
|
||||
"Nats" => Coordinator::Nats(NatsCoordinator {
|
||||
addresses: Map::new(vec!["127.0.0.1:4222".to_string()]),
|
||||
use_tls: false,
|
||||
..Default::default()
|
||||
}),
|
||||
"Redis" => Coordinator::Redis(RedisStore {
|
||||
url: "redis://127.0.0.1".to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
"Nats" => {
|
||||
crate::utils::containers::ensure_nats().await;
|
||||
Coordinator::Nats(NatsCoordinator {
|
||||
addresses: Map::new(vec!["127.0.0.1:4222".to_string()]),
|
||||
use_tls: false,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
"Redis" => {
|
||||
crate::utils::containers::ensure_redis().await;
|
||||
Coordinator::Redis(RedisStore {
|
||||
url: "redis://127.0.0.1".to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
_ => panic!("Unsupported coordinator type: {}", coordinator_id),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use types::id::Id;
|
||||
|
||||
pub async fn test() {
|
||||
println!("Running directory integration tests...");
|
||||
crate::utils::containers::ensure_openldap().await;
|
||||
let test = TestServerBuilder::new("directory_integration_test")
|
||||
.await
|
||||
.with_default_listeners()
|
||||
|
||||
@@ -12,6 +12,7 @@ use registry::{
|
||||
|
||||
pub async fn test() {
|
||||
println!("Running LDAP directory tests...");
|
||||
crate::utils::containers::ensure_openldap().await;
|
||||
let mut config = ldap_test_directory();
|
||||
|
||||
// Test bind authentication
|
||||
|
||||
@@ -13,6 +13,7 @@ use registry::{schema::structs, types::map::Map};
|
||||
|
||||
pub async fn test() {
|
||||
println!("Running OIDC directory tests...");
|
||||
crate::utils::containers::ensure_keycloak().await;
|
||||
let config = structs::OidcDirectory {
|
||||
description: "Test OIDC directory".to_string(),
|
||||
issuer_url: "http://localhost:9080/realms/stalwart".to_string(),
|
||||
|
||||
455
tests/src/utils/containers.rs
Normal file
455
tests/src/utils/containers.rs
Normal file
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use testcontainers::{
|
||||
ContainerAsync, GenericBuildableImage, GenericImage, ImageExt, ReuseDirective,
|
||||
core::{CmdWaitFor, ExecCommand, Host, IntoContainerPort, WaitFor},
|
||||
runners::{AsyncBuilder, AsyncRunner},
|
||||
};
|
||||
use tokio::{net::TcpStream, sync::OnceCell};
|
||||
|
||||
const ACME_NETWORK: &str = "stalwart-test-acme";
|
||||
|
||||
const READY_TIMEOUT: Duration = Duration::from_secs(180);
|
||||
|
||||
static FOUNDATIONDB: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static POSTGRES: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static MYSQL: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static REDIS: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static NATS: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static MINIO: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static OPENSEARCH: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static MEILISEARCH: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static KEYCLOAK: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static OPENLDAP: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static CHALLTESTSRV: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static PEBBLE: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
static POWERDNS: OnceCell<ContainerAsync<GenericImage>> = OnceCell::const_new();
|
||||
|
||||
const POWERDNS_ZONE_INIT: &str = r#"set -e
|
||||
for i in $(seq 1 60); do
|
||||
pdnsutil list-all-zones >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
if pdnsutil list-zone stalwart.test >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
pdnsutil create-zone stalwart.test ns1.stalwart.test
|
||||
pdnsutil set-kind stalwart.test native
|
||||
pdnsutil replace-rrset stalwart.test '' SOA 'ns1.stalwart.test. admin.stalwart.test. 2024010101 3600 900 604800 86400'
|
||||
pdnsutil add-record stalwart.test 'ns1' A '127.0.0.1'
|
||||
pdnsutil add-record stalwart.test '' A '127.0.0.1'
|
||||
pdnsutil add-record stalwart.test '' MX '10 mail.stalwart.test.'
|
||||
pdnsutil add-record stalwart.test 'mail' A '127.0.0.1'
|
||||
pdnsutil import-tsig-key stalwart-update-key hmac-sha256 'c3RhbHdhcnQtdGVzdC10c2lnLXNlY3JldC1rZXkxMjM0NTY3ODkw'
|
||||
pdnsutil activate-tsig-key stalwart.test stalwart-update-key primary
|
||||
pdnsutil set-meta stalwart.test TSIG-ALLOW-DNSUPDATE stalwart-update-key
|
||||
pdnsutil set-meta stalwart.test ALLOW-DNSUPDATE-FROM '0.0.0.0/0'
|
||||
"#;
|
||||
|
||||
pub async fn ensure_foundationdb() {
|
||||
let container = FOUNDATIONDB
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("foundationdb/foundationdb", "7.4.6")
|
||||
.with_env_var("FDB_NETWORKING_MODE", "container")
|
||||
.with_mapped_port(4500, 4500.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-foundationdb")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start FoundationDB container")
|
||||
})
|
||||
.await;
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if fdbcli(container, "status minimal")
|
||||
.await
|
||||
.contains("The database is available")
|
||||
{
|
||||
return;
|
||||
}
|
||||
let created = fdbcli(container, "configure new single memory").await;
|
||||
if created.contains("Database created") || created.contains("Already exists") {
|
||||
continue;
|
||||
}
|
||||
if start.elapsed() > READY_TIMEOUT {
|
||||
panic!("Timed out configuring FoundationDB: {created}");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn fdbcli(container: &ContainerAsync<GenericImage>, command: &str) -> String {
|
||||
let mut result = container
|
||||
.exec(
|
||||
ExecCommand::new(["fdbcli", "--exec", command, "--timeout", "5"])
|
||||
.with_cmd_ready_condition(CmdWaitFor::exit()),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to exec fdbcli");
|
||||
let stdout = result.stdout_to_vec().await.unwrap_or_default();
|
||||
let stderr = result.stderr_to_vec().await.unwrap_or_default();
|
||||
format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&stdout),
|
||||
String::from_utf8_lossy(&stderr)
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn ensure_postgres() {
|
||||
POSTGRES
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("postgres", "16-alpine")
|
||||
.with_wait_for(WaitFor::message_on_stderr(
|
||||
"database system is ready to accept connections",
|
||||
))
|
||||
.with_wait_for(WaitFor::message_on_stderr(
|
||||
"database system is ready to accept connections",
|
||||
))
|
||||
.with_env_var("POSTGRES_USER", "stalwart")
|
||||
.with_env_var("POSTGRES_PASSWORD", "stalwart")
|
||||
.with_env_var("POSTGRES_DB", "stalwart")
|
||||
.with_mapped_port(5432, 5432.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-postgres")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start PostgreSQL container")
|
||||
})
|
||||
.await;
|
||||
wait_for_tcp(5432).await;
|
||||
}
|
||||
|
||||
pub async fn ensure_mysql() {
|
||||
MYSQL
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("mysql", "8.0")
|
||||
.with_wait_for(WaitFor::message_on_stderr("port: 3306 MySQL"))
|
||||
.with_env_var("MYSQL_ROOT_PASSWORD", "password")
|
||||
.with_env_var("MYSQL_DATABASE", "stalwart")
|
||||
.with_cmd(["--default-authentication-plugin=mysql_native_password"])
|
||||
.with_mapped_port(3307, 3306.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-mysql")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start MySQL container")
|
||||
})
|
||||
.await;
|
||||
wait_for_tcp(3307).await;
|
||||
}
|
||||
|
||||
pub async fn ensure_redis() {
|
||||
REDIS
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("redis", "7-alpine")
|
||||
.with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
|
||||
.with_cmd(["redis-server", "--save", "", "--appendonly", "no"])
|
||||
.with_mapped_port(6379, 6379.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-redis")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start Redis container")
|
||||
})
|
||||
.await;
|
||||
wait_for_tcp(6379).await;
|
||||
}
|
||||
|
||||
pub async fn ensure_nats() {
|
||||
NATS.get_or_init(|| async {
|
||||
GenericImage::new("nats", "latest")
|
||||
.with_wait_for(WaitFor::message_on_stderr("Server is ready"))
|
||||
.with_cmd(["--addr", "0.0.0.0", "--port", "4222", "--http_port", "8222"])
|
||||
.with_mapped_port(4222, 4222.tcp())
|
||||
.with_mapped_port(8222, 8222.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-nats")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start NATS container")
|
||||
})
|
||||
.await;
|
||||
wait_for_tcp(4222).await;
|
||||
}
|
||||
|
||||
pub async fn ensure_minio() {
|
||||
MINIO
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("minio/minio", "latest")
|
||||
.with_env_var("MINIO_ROOT_USER", "minioadmin")
|
||||
.with_env_var("MINIO_ROOT_PASSWORD", "minioadmin")
|
||||
.with_cmd(["server", "/data", "--console-address", ":9001"])
|
||||
.with_mapped_port(9000, 9000.tcp())
|
||||
.with_mapped_port(9001, 9001.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-minio")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start MinIO container")
|
||||
})
|
||||
.await;
|
||||
wait_for_http("http://localhost:9000/minio/health/live").await;
|
||||
create_minio_bucket().await;
|
||||
}
|
||||
|
||||
pub async fn ensure_opensearch() {
|
||||
OPENSEARCH
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("opensearchproject/opensearch", "2")
|
||||
.with_env_var("discovery.type", "single-node")
|
||||
.with_env_var("DISABLE_SECURITY_PLUGIN", "true")
|
||||
.with_env_var("OPENSEARCH_JAVA_OPTS", "-Xms1g -Xmx1g")
|
||||
.with_env_var("DISABLE_INSTALL_DEMO_CONFIG", "true")
|
||||
.with_mapped_port(9200, 9200.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-opensearch")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start OpenSearch container")
|
||||
})
|
||||
.await;
|
||||
wait_for_http("http://localhost:9200").await;
|
||||
}
|
||||
|
||||
pub async fn ensure_meilisearch() {
|
||||
MEILISEARCH
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("getmeili/meilisearch", "latest")
|
||||
.with_env_var("MEILI_ENV", "development")
|
||||
.with_env_var("MEILI_NO_ANALYTICS", "true")
|
||||
.with_env_var("MEILI_MASTER_KEY", "stalwart-master-key")
|
||||
.with_mapped_port(7700, 7700.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-meilisearch")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start Meilisearch container")
|
||||
})
|
||||
.await;
|
||||
wait_for_http("http://localhost:7700/health").await;
|
||||
}
|
||||
|
||||
pub async fn ensure_keycloak() {
|
||||
KEYCLOAK
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("quay.io/keycloak/keycloak", "latest")
|
||||
.with_env_var("KC_BOOTSTRAP_ADMIN_USERNAME", "admin")
|
||||
.with_env_var("KC_BOOTSTRAP_ADMIN_PASSWORD", "admin")
|
||||
.with_env_var("KC_HTTP_PORT", "9080")
|
||||
.with_env_var("KC_HEALTH_ENABLED", "true")
|
||||
.with_cmd(["start-dev", "--import-realm"])
|
||||
.with_copy_to(
|
||||
"/opt/keycloak/data/import/stalwart-realm.json",
|
||||
include_bytes!("../../docker/keycloak/stalwart-realm.json").to_vec(),
|
||||
)
|
||||
.with_mapped_port(9080, 9080.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-keycloak")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start Keycloak container")
|
||||
})
|
||||
.await;
|
||||
wait_for_http("http://localhost:9080/realms/stalwart/.well-known/openid-configuration").await;
|
||||
}
|
||||
|
||||
pub async fn ensure_acme() {
|
||||
ensure_challtestsrv().await;
|
||||
ensure_pebble().await;
|
||||
}
|
||||
|
||||
async fn ensure_challtestsrv() {
|
||||
CHALLTESTSRV
|
||||
.get_or_init(|| async {
|
||||
let image = GenericBuildableImage::new("stalwart-test-challtestsrv", "local")
|
||||
.with_dockerfile_string(include_str!("../../docker/pebble/Dockerfile.challtestsrv"))
|
||||
.build_image()
|
||||
.await
|
||||
.expect("Failed to build challtestsrv image");
|
||||
image
|
||||
.with_network(ACME_NETWORK)
|
||||
.with_host("host.docker.internal", Host::HostGateway)
|
||||
.with_mapped_port(8055, 8055.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-challtestsrv")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start challtestsrv container")
|
||||
})
|
||||
.await;
|
||||
wait_for_tcp(8055).await;
|
||||
}
|
||||
|
||||
async fn ensure_pebble() {
|
||||
PEBBLE
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("ghcr.io/letsencrypt/pebble", "latest")
|
||||
.with_env_var("PEBBLE_VA_NOSLEEP", "1")
|
||||
.with_env_var("PEBBLE_WFE_NONCEREJECT", "0")
|
||||
.with_cmd([
|
||||
"-config",
|
||||
"/test/config/pebble-config.json",
|
||||
"-dnsserver",
|
||||
"stalwart-test-challtestsrv:8053",
|
||||
])
|
||||
.with_copy_to(
|
||||
"/test/config/pebble-config.json",
|
||||
include_bytes!("../../docker/pebble/pebble-config.json").to_vec(),
|
||||
)
|
||||
.with_network(ACME_NETWORK)
|
||||
.with_host("host.docker.internal", Host::HostGateway)
|
||||
.with_mapped_port(14000, 14000.tcp())
|
||||
.with_mapped_port(15000, 15000.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-pebble")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start Pebble container")
|
||||
})
|
||||
.await;
|
||||
wait_for_tcp(14000).await;
|
||||
}
|
||||
|
||||
pub async fn ensure_powerdns() {
|
||||
let container = POWERDNS
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("powerdns/pdns-auth-49", "latest")
|
||||
.with_wait_for(WaitFor::message_on_stderr("Creating backend connection"))
|
||||
.with_env_var("PDNS_AUTH_API_KEY", "stalwart-api-key")
|
||||
.with_copy_to(
|
||||
"/etc/powerdns/pdns.d/stalwart.conf",
|
||||
include_bytes!("../../docker/powerdns/pdns.conf").to_vec(),
|
||||
)
|
||||
.with_mapped_port(5300, 53.tcp())
|
||||
.with_mapped_port(5300, 53.udp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-powerdns")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start PowerDNS container")
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut result = container
|
||||
.exec(
|
||||
ExecCommand::new(["bash", "-c", POWERDNS_ZONE_INIT])
|
||||
.with_cmd_ready_condition(CmdWaitFor::exit()),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to exec PowerDNS zone init");
|
||||
if result.exit_code().await.ok().flatten() != Some(0) {
|
||||
let stdout =
|
||||
String::from_utf8_lossy(&result.stdout_to_vec().await.unwrap_or_default()).into_owned();
|
||||
let stderr =
|
||||
String::from_utf8_lossy(&result.stderr_to_vec().await.unwrap_or_default()).into_owned();
|
||||
panic!("PowerDNS zone init failed:\n{stdout}\n{stderr}");
|
||||
}
|
||||
wait_for_tcp(5300).await;
|
||||
}
|
||||
|
||||
pub async fn ensure_openldap() {
|
||||
const BOOTSTRAP_DIR: &str = "/container/service/slapd/assets/config/bootstrap/ldif/custom";
|
||||
OPENLDAP
|
||||
.get_or_init(|| async {
|
||||
GenericImage::new("osixia/openldap", "1.5.0")
|
||||
.with_wait_for(WaitFor::message_on_stderr("slapd starting"))
|
||||
.with_env_var("LDAP_ORGANISATION", "Stalwart Test")
|
||||
.with_env_var("LDAP_DOMAIN", "stalwart.test")
|
||||
.with_env_var("LDAP_BASE_DN", "dc=stalwart,dc=test")
|
||||
.with_env_var("LDAP_ADMIN_PASSWORD", "admin")
|
||||
.with_env_var("LDAP_TLS", "false")
|
||||
.with_copy_to(
|
||||
format!("{BOOTSTRAP_DIR}/50-users.ldif"),
|
||||
include_bytes!("../../docker/ldap/50-users.ldif").to_vec(),
|
||||
)
|
||||
.with_copy_to(
|
||||
format!("{BOOTSTRAP_DIR}/60-groups.ldif"),
|
||||
include_bytes!("../../docker/ldap/60-groups.ldif").to_vec(),
|
||||
)
|
||||
.with_mapped_port(389, 389.tcp())
|
||||
.with_startup_timeout(READY_TIMEOUT)
|
||||
.with_container_name("stalwart-test-openldap")
|
||||
.with_reuse(ReuseDirective::Always)
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start OpenLDAP container")
|
||||
})
|
||||
.await;
|
||||
wait_for_tcp(389).await;
|
||||
}
|
||||
|
||||
async fn create_minio_bucket() {
|
||||
use s3::{Bucket, BucketConfiguration, Region, creds::Credentials};
|
||||
|
||||
let region = Region::Custom {
|
||||
region: "eu-central-1".to_string(),
|
||||
endpoint: "http://localhost:9000".to_string(),
|
||||
};
|
||||
let credentials = Credentials::new(Some("minioadmin"), Some("minioadmin"), None, None, None)
|
||||
.expect("Failed to build MinIO credentials");
|
||||
|
||||
match Bucket::create_with_path_style(
|
||||
"stalwart",
|
||||
region,
|
||||
credentials,
|
||||
BucketConfiguration::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) if response.success() => {}
|
||||
Ok(_) => {}
|
||||
Err(s3::error::S3Error::HttpFailWithBody(409, _)) => {}
|
||||
Err(err) => panic!("Failed to create MinIO bucket: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_tcp(port: u16) {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if TcpStream::connect(("127.0.0.1", port)).await.is_ok() {
|
||||
return;
|
||||
}
|
||||
if start.elapsed() > READY_TIMEOUT {
|
||||
panic!("Timed out waiting for TCP port {port}");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_http(url: &str) {
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("Failed to build HTTP client");
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Ok(response) = client.get(url).send().await
|
||||
&& response.status().is_success()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if start.elapsed() > READY_TIMEOUT {
|
||||
panic!("Timed out waiting for {url}");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
pub mod account;
|
||||
pub mod cleanup;
|
||||
pub mod containers;
|
||||
pub mod dns;
|
||||
pub mod http;
|
||||
pub mod http_server;
|
||||
|
||||
@@ -113,7 +113,8 @@ impl TestServerBuilder {
|
||||
"running `STORE=<store_type> cargo test`"
|
||||
)),
|
||||
&path,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let store = Store::build(data_store).await.unwrap();
|
||||
|
||||
store.create_tables().await.unwrap();
|
||||
|
||||
@@ -14,9 +14,9 @@ use registry::{
|
||||
prelude::Object,
|
||||
structs::{
|
||||
BlobStore, DataStore, ElasticSearchStore, FileSystemStore, FoundationDbStore, HttpAuth,
|
||||
HttpAuthBasic, InMemoryStore, MeilisearchStore, MySqlStore, PostgreSqlStore,
|
||||
RedisStore, RocksDbStore, S3Store, S3StoreCustomRegion, S3StoreRegion, SearchStore,
|
||||
SecretKey, SecretKeyOptional, SecretKeyValue, SqliteStore,
|
||||
HttpAuthBasic, HttpAuthBearer, InMemoryStore, MeilisearchStore, MySqlStore,
|
||||
PostgreSqlStore, RedisStore, RocksDbStore, S3Store, S3StoreCustomRegion, S3StoreRegion,
|
||||
SearchStore, SecretKey, SecretKeyOptional, SecretKeyValue, SqliteStore,
|
||||
},
|
||||
},
|
||||
types::{EnumImpl, duration::Duration},
|
||||
@@ -35,21 +35,21 @@ pub trait RegistryEnvStores {
|
||||
impl RegistryEnvStores for RegistryStore {
|
||||
async fn insert_stores_from_env(&self) {
|
||||
let path = self.path().as_os_str().to_str().unwrap();
|
||||
let search_store = std::env::var("SEARCH_STORE")
|
||||
.map(|store| SearchStoreType::parse(&store).expect("Invalid store type"))
|
||||
.map(|store| build_search_store(store, path))
|
||||
.map(Object::from)
|
||||
.ok();
|
||||
let blob_store = std::env::var("BLOB_STORE")
|
||||
.map(|store| BlobStoreType::parse(&store).expect("Invalid store type"))
|
||||
.map(|store| build_blob_store(store, path))
|
||||
.map(Object::from)
|
||||
.ok();
|
||||
let in_memory = std::env::var("MEMORY_STORE")
|
||||
.map(|store| InMemoryStoreType::parse(&store).expect("Invalid store type"))
|
||||
.map(|store| build_in_memory_store(store, path))
|
||||
.map(Object::from)
|
||||
.ok();
|
||||
let mut search_store = None;
|
||||
if let Ok(store) = std::env::var("SEARCH_STORE") {
|
||||
let store = SearchStoreType::parse(&store).expect("Invalid store type");
|
||||
search_store = Some(Object::from(build_search_store(store, path).await));
|
||||
}
|
||||
let mut blob_store = None;
|
||||
if let Ok(store) = std::env::var("BLOB_STORE") {
|
||||
let store = BlobStoreType::parse(&store).expect("Invalid store type");
|
||||
blob_store = Some(Object::from(build_blob_store(store, path).await));
|
||||
}
|
||||
let mut in_memory = None;
|
||||
if let Ok(store) = std::env::var("MEMORY_STORE") {
|
||||
let store = InMemoryStoreType::parse(&store).expect("Invalid store type");
|
||||
in_memory = Some(Object::from(build_in_memory_store(store, path).await));
|
||||
}
|
||||
|
||||
for store in [search_store, blob_store, in_memory].into_iter().flatten() {
|
||||
self.write(RegistryWrite::insert(&store))
|
||||
@@ -60,7 +60,7 @@ impl RegistryEnvStores for RegistryStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_data_store(typ: DataStoreType, path: &str) -> DataStore {
|
||||
pub async fn build_data_store(typ: DataStoreType, path: &str) -> DataStore {
|
||||
match typ {
|
||||
DataStoreType::RocksDb => DataStore::RocksDb(RocksDbStore {
|
||||
path: format!("{path}/rocks.db"),
|
||||
@@ -70,49 +70,61 @@ pub fn build_data_store(typ: DataStoreType, path: &str) -> DataStore {
|
||||
path: format!("{path}/sqlite.db"),
|
||||
..Default::default()
|
||||
}),
|
||||
DataStoreType::FoundationDb => DataStore::FoundationDb(FoundationDbStore::default()),
|
||||
DataStoreType::PostgreSql => DataStore::PostgreSql(PostgreSqlStore {
|
||||
host: "localhost".into(),
|
||||
port: 5432,
|
||||
auth_username: "stalwart".to_string().into(),
|
||||
auth_secret: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "stalwart".into(),
|
||||
}),
|
||||
database: "stalwart".into(),
|
||||
use_tls: false,
|
||||
allow_invalid_certs: true,
|
||||
..Default::default()
|
||||
}),
|
||||
DataStoreType::MySql => DataStore::MySql(MySqlStore {
|
||||
host: "localhost".into(),
|
||||
port: 3307,
|
||||
auth_username: "root".to_string().into(),
|
||||
auth_secret: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "password".into(),
|
||||
}),
|
||||
database: "stalwart".into(),
|
||||
use_tls: false,
|
||||
allow_invalid_certs: true,
|
||||
..Default::default()
|
||||
}),
|
||||
DataStoreType::FoundationDb => {
|
||||
crate::utils::containers::ensure_foundationdb().await;
|
||||
DataStore::FoundationDb(FoundationDbStore::default())
|
||||
}
|
||||
DataStoreType::PostgreSql => {
|
||||
crate::utils::containers::ensure_postgres().await;
|
||||
DataStore::PostgreSql(PostgreSqlStore {
|
||||
host: "localhost".into(),
|
||||
port: 5432,
|
||||
auth_username: "stalwart".to_string().into(),
|
||||
auth_secret: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "stalwart".into(),
|
||||
}),
|
||||
database: "stalwart".into(),
|
||||
use_tls: false,
|
||||
allow_invalid_certs: true,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
DataStoreType::MySql => {
|
||||
crate::utils::containers::ensure_mysql().await;
|
||||
DataStore::MySql(MySqlStore {
|
||||
host: "localhost".into(),
|
||||
port: 3307,
|
||||
auth_username: "root".to_string().into(),
|
||||
auth_secret: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "password".into(),
|
||||
}),
|
||||
database: "stalwart".into(),
|
||||
use_tls: false,
|
||||
allow_invalid_certs: true,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_blob_store(typ: BlobStoreType, path: &str) -> BlobStore {
|
||||
async fn build_blob_store(typ: BlobStoreType, path: &str) -> BlobStore {
|
||||
match typ {
|
||||
BlobStoreType::S3 => BlobStore::S3(S3Store {
|
||||
access_key: "minioadmin".to_string().into(),
|
||||
bucket: "stalwart".into(),
|
||||
region: S3StoreRegion::Custom(S3StoreCustomRegion {
|
||||
custom_endpoint: "http://localhost:9000".into(),
|
||||
custom_region: "eu-central-1".into(),
|
||||
}),
|
||||
secret_key: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "minioadmin".into(),
|
||||
}),
|
||||
allow_invalid_certs: true,
|
||||
..Default::default()
|
||||
}),
|
||||
BlobStoreType::S3 => {
|
||||
crate::utils::containers::ensure_minio().await;
|
||||
BlobStore::S3(S3Store {
|
||||
access_key: "minioadmin".to_string().into(),
|
||||
bucket: "stalwart".into(),
|
||||
region: S3StoreRegion::Custom(S3StoreCustomRegion {
|
||||
custom_endpoint: "http://localhost:9000".into(),
|
||||
custom_region: "eu-central-1".into(),
|
||||
}),
|
||||
secret_key: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "minioadmin".into(),
|
||||
}),
|
||||
allow_invalid_certs: true,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
BlobStoreType::FileSystem => BlobStore::FileSystem(FileSystemStore {
|
||||
path: path.to_string(),
|
||||
..Default::default()
|
||||
@@ -121,35 +133,49 @@ fn build_blob_store(typ: BlobStoreType, path: &str) -> BlobStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_in_memory_store(typ: InMemoryStoreType, _path: &str) -> InMemoryStore {
|
||||
async fn build_in_memory_store(typ: InMemoryStoreType, _path: &str) -> InMemoryStore {
|
||||
match typ {
|
||||
InMemoryStoreType::Redis => InMemoryStore::Redis(RedisStore {
|
||||
url: "redis://127.0.0.1".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
InMemoryStoreType::Redis => {
|
||||
crate::utils::containers::ensure_redis().await;
|
||||
InMemoryStore::Redis(RedisStore {
|
||||
url: "redis://127.0.0.1".into(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_search_store(typ: SearchStoreType, _path: &str) -> SearchStore {
|
||||
async fn build_search_store(typ: SearchStoreType, _path: &str) -> SearchStore {
|
||||
match typ {
|
||||
SearchStoreType::ElasticSearch => SearchStore::ElasticSearch(ElasticSearchStore {
|
||||
url: "https://localhost:9200".into(),
|
||||
allow_invalid_certs: true,
|
||||
http_auth: HttpAuth::Basic(HttpAuthBasic {
|
||||
username: "elastic".into(),
|
||||
secret: SecretKey::Value(SecretKeyValue {
|
||||
secret: "changeme".into(),
|
||||
SearchStoreType::ElasticSearch => {
|
||||
crate::utils::containers::ensure_opensearch().await;
|
||||
SearchStore::ElasticSearch(ElasticSearchStore {
|
||||
url: "http://localhost:9200".into(),
|
||||
allow_invalid_certs: true,
|
||||
http_auth: HttpAuth::Basic(HttpAuthBasic {
|
||||
username: "elastic".into(),
|
||||
secret: SecretKey::Value(SecretKeyValue {
|
||||
secret: "changeme".into(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
SearchStoreType::Meilisearch => SearchStore::Meilisearch(MeilisearchStore {
|
||||
url: "http://localhost:7700".into(),
|
||||
allow_invalid_certs: true,
|
||||
poll_interval: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
SearchStoreType::Meilisearch => {
|
||||
crate::utils::containers::ensure_meilisearch().await;
|
||||
SearchStore::Meilisearch(MeilisearchStore {
|
||||
url: "http://localhost:7700".into(),
|
||||
allow_invalid_certs: true,
|
||||
poll_interval: Duration::from_millis(100),
|
||||
http_auth: HttpAuth::Bearer(HttpAuthBearer {
|
||||
bearer_token: SecretKey::Value(SecretKeyValue {
|
||||
secret: "stalwart-master-key".into(),
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user