Files
Stalwart/tests/src/cluster/broadcast.rs
2026-06-11 18:08:13 +02:00

219 lines
6.6 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
imap::idle,
utils::{
imap::{ImapConnection, Type},
server::TestServerBuilder,
},
};
use imap_proto::ResponseType;
use registry::{
schema::{
enums::NetworkListenerProtocol,
prelude::{ObjectType, Property, SocketAddr},
structs::{
ClusterListenerGroup, ClusterListenerGroupProperties, ClusterRole, ClusterTaskGroup,
Coordinator, NatsCoordinator, NetworkListener, RedisStore,
},
},
types::map::Map,
};
use serde_json::json;
use std::str::FromStr;
use store::registry::RegistryQuery;
use types::id::Id;
pub const NUM_NODES: usize = 3;
#[tokio::test(flavor = "multi_thread")]
pub async fn cluster_tests() {
println!("Running cluster broadcast tests...");
let mut servers = Vec::with_capacity(NUM_NODES);
let coordinator_id = std::env::var("COORDINATOR").expect(concat!(
"Missing coordinator type. Try running `STORE=<store_type> ",
"COORDINATOR=<coordinator_type> cargo test`"
));
let coordinator = match coordinator_id.as_str() {
"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),
};
// Create initial server
let test = TestServerBuilder::new("cluster_test_0")
.await
.with_object(coordinator)
.await
.with_listener(NetworkListenerProtocol::Http, "http_0", 11000, true)
.await
.with_imap_listener(12000)
.await
.with_listener(NetworkListenerProtocol::Lmtp, "lmtp_0", 11200, false)
.await
.build()
.await;
let admin = test.account("admin");
admin.mta_no_auth().await;
let account = admin
.create_user_account(
"jdoe@example.com",
"this is john's secret",
"John's account",
&[],
vec![],
)
.await;
admin.reload_settings().await;
// Create listeners
let mut listeners = vec![
test.server
.registry()
.query::<Vec<Id>>(RegistryQuery::new(ObjectType::NetworkListener))
.await
.unwrap(),
];
for node_id in 1..NUM_NODES {
let http_listener_id = admin
.registry_create_object(NetworkListener {
name: format!("http_{}", node_id),
bind: Map::new(vec![
SocketAddr::from_str(&format!("127.0.0.1:1100{node_id}")).unwrap(),
]),
protocol: NetworkListenerProtocol::Http,
tls_implicit: true,
use_tls: true,
..Default::default()
})
.await;
let imap_listener_id = admin
.registry_create_object(NetworkListener {
name: format!("imap_{}", node_id),
bind: Map::new(vec![
SocketAddr::from_str(&format!("127.0.0.1:1200{node_id}")).unwrap(),
]),
protocol: NetworkListenerProtocol::Imap,
tls_implicit: false,
use_tls: true,
..Default::default()
})
.await;
listeners.push(vec![http_listener_id, imap_listener_id]);
}
// Create node roles
for (role_id, listener_ids) in listeners.into_iter().enumerate() {
admin
.registry_create_object(ClusterRole {
name: format!("role_{role_id}"),
listeners: ClusterListenerGroup::EnableSome(ClusterListenerGroupProperties {
listener_ids: Map::new(listener_ids),
}),
tasks: ClusterTaskGroup::EnableAll,
description: None,
})
.await;
}
servers.push(test);
// Build additional servers
for node_id in 1..NUM_NODES {
let test = TestServerBuilder::new_with_role(
&format!("cluster_test_{node_id}"),
format!("mail-{node_id}.example.com"),
Some(format!("role_{node_id}")),
false,
)
.await
.build_with_opts(false)
.await;
// Verify that the server was assigned the correct node id
assert_eq!(test.server.registry().node_id(), node_id as u16);
servers.push(test);
}
// Verify cross-cluster cache invalidations
let admin = servers[0].account("admin");
let server1 = &servers[1].server;
let server2 = &servers[2].server;
let account_id = account.id().document_id();
assert_eq!(
server1
.account(account_id)
.await
.unwrap()
.description
.as_deref(),
Some("John's account")
);
assert_eq!(
server2
.account(account_id)
.await
.unwrap()
.description
.as_deref(),
Some("John's account")
);
admin
.registry_update_object(
ObjectType::Account,
account.id(),
json!({
Property::Description: "John Doe"
}),
)
.await;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert_eq!(
server1
.account(account_id)
.await
.unwrap()
.description
.as_deref(),
Some("John Doe")
);
assert_eq!(
server2
.account(account_id)
.await
.unwrap()
.description
.as_deref(),
Some("John Doe")
);
// Run IMAP idle tests across nodes
let mut node1_client = imap_client("jdoe@example.com", "this is john's secret", 1).await;
let mut node2_client = imap_client("jdoe@example.com", "this is john's secret", 2).await;
idle::test(&mut node1_client, &mut node2_client, true).await;
}
async fn imap_client(login: &str, secret: &str, node_id: u32) -> ImapConnection {
let mut conn = ImapConnection::connect_to(b"A1 ", format!("127.0.0.1:1200{node_id}")).await;
conn.assert_read(Type::Untagged, ResponseType::Ok).await;
conn.authenticate(login, secret).await;
conn
}