Nats + Redis PubSub cluster updates replacing Gossip protocol

This commit is contained in:
mdecimus
2025-05-14 19:43:47 +02:00
parent 7ec5701af8
commit 839b7189fa
65 changed files with 1807 additions and 1350 deletions

View File

@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::net::IpAddr;
use crate::imap::idle;
use super::ClusterTest;
pub async fn test(cluster: &ClusterTest) {
println!("Running cluster broadcast tests...");
// Run IMAP idle tests across nodes
let mut node1_client = cluster.imap_client("john", 1).await;
let mut node2_client = cluster.imap_client("john", 2).await;
idle::test(&mut node1_client, &mut node2_client, true).await;
// Test event broadcast
let server1 = cluster.server(1);
let server2 = cluster.server(2);
let test_ip: IpAddr = "8.8.8.8".parse().unwrap();
assert!(!server1.is_ip_blocked(&test_ip));
assert!(!server2.is_ip_blocked(&test_ip));
server1.block_ip(test_ip).await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert!(server1.is_ip_blocked(&test_ip));
assert!(server2.is_ip_blocked(&test_ip));
}

362
tests/src/cluster/mod.rs Normal file
View File

@@ -0,0 +1,362 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{path::PathBuf, sync::Arc, time::Duration};
use common::{
Caches, Core, Data, Inner, Server,
config::{
server::{Listeners, ServerProtocol},
telemetry::Telemetry,
},
core::BuildServer,
manager::{
boot::build_ipc,
config::{ConfigManager, Patterns},
},
};
use http::HttpSessionManager;
use imap::core::ImapSessionManager;
use imap_proto::ResponseType;
use jmap_client::client::{Client, Credentials};
use managesieve::core::ManageSieveSessionManager;
use pop3::Pop3SessionManager;
use services::{SpawnServices, broadcast::subscriber::spawn_broadcast_subscriber};
use smtp::{SpawnQueueManager, core::SmtpSessionManager};
use store::Stores;
use tokio::sync::watch;
use utils::config::Config;
use crate::{
AssertConfig, TEST_USERS, add_test_certs,
directory::internal::TestInternalDirectory,
imap::{ImapConnection, Type},
jmap::enterprise::EnterpriseCore,
};
pub mod broadcast;
pub mod stress;
pub const NUM_NODES: usize = 3;
#[tokio::test(flavor = "multi_thread")]
pub async fn cluster_tests() {
let params = init_cluster_tests(true).await;
//stress::test(params.server.clone(), params.client).await;
broadcast::test(&params).await;
}
#[allow(dead_code)]
pub struct ClusterTest {
servers: Vec<Server>,
shutdown_txs: Vec<watch::Sender<bool>>,
}
async fn init_cluster_tests(delete_if_exists: bool) -> ClusterTest {
// Load and parse config
let store_id = std::env::var("STORE").expect(
"Missing store type. Try running `STORE=<store_type> PUBSUB=<pubsub_type> cargo test`",
);
let pubsub_id = std::env::var("PUBSUB").expect(
"Missing store type. Try running `STORE=<store_type> PUBSUB=<pubsub_type> cargo test`",
);
let mut pubsub_config = match pubsub_id.as_str() {
"nats" => Config::new(SERVER_NATS).unwrap(),
"redis" => Config::new(SERVER_REDIS).unwrap(),
_ => panic!("Unsupported pubsub type: {}", pubsub_id),
};
// Build configs
let mut configs = Vec::with_capacity(NUM_NODES);
for node_id in 0..NUM_NODES {
let mut config = Config::new(
add_test_certs(SERVER)
.replace("{STORE}", &store_id)
.replace("{PUBSUB}", &pubsub_id)
.replace("{NODE_ID}", &node_id.to_string())
.replace(
"{LEVEL}",
&std::env::var("LOG").unwrap_or_else(|_| "disable".to_string()),
),
)
.unwrap();
config.resolve_all_macros().await;
configs.push(config);
}
// Build stores
let stores = Stores::parse_all(configs.first_mut().unwrap(), false).await;
// Build servers
let mut servers = Vec::with_capacity(NUM_NODES);
let mut shutdown_txs = Vec::with_capacity(NUM_NODES);
for config in configs {
let mut stores = stores.clone();
stores.pubsub_stores = Stores::parse(&mut pubsub_config).await.pubsub_stores;
let (server, shutdown_tx) = build_server(config, stores).await;
servers.push(server);
shutdown_txs.push(shutdown_tx);
}
let store = servers.first().unwrap().store().clone();
if delete_if_exists {
store.destroy().await;
}
// Create test users
for (account, secret, name, email) in TEST_USERS {
let _account_id = store
.create_test_user(account, secret, name, &[email])
.await;
}
ClusterTest {
servers,
shutdown_txs,
}
}
impl ClusterTest {
pub async fn jmap_client(&self, login: &str, node_id: u32) -> Client {
Client::new()
.credentials(Credentials::basic(login, find_account_secret(login)))
.timeout(Duration::from_secs(3600))
.accept_invalid_certs(true)
.connect(&format!("https://127.0.0.1:1800{node_id}"))
.await
.unwrap()
}
pub async fn imap_client(&self, login: &str, node_id: u32) -> ImapConnection {
let mut conn = ImapConnection::connect_to(b"A1 ", format!("127.0.0.1:1900{node_id}")).await;
conn.assert_read(Type::Untagged, ResponseType::Ok).await;
conn.authenticate(login, find_account_secret(login)).await;
conn
}
pub fn server(&self, node_id: usize) -> &Server {
self.servers
.get(node_id)
.unwrap_or_else(|| panic!("No server found for node ID: {}", node_id))
}
}
fn find_account_secret(login: &str) -> &str {
TEST_USERS
.iter()
.find(|(account, _, _, _)| account == &login)
.map(|(_, secret, _, _)| secret)
.unwrap_or_else(|| panic!("No account found for login: {}", login))
}
async fn build_server(mut config: Config, stores: Stores) -> (Server, watch::Sender<bool>) {
// Parse servers
let mut servers = Listeners::parse(&mut config);
// Bind ports and drop privileges
servers.bind_and_drop_priv(&mut config);
// Parse core
let config_manager = ConfigManager {
cfg_local: Default::default(),
cfg_local_path: PathBuf::new(),
cfg_local_patterns: Patterns::parse(&mut config).into(),
cfg_store: config
.value("storage.data")
.and_then(|id| stores.stores.get(id))
.cloned()
.unwrap_or_default(),
};
let tracers = Telemetry::parse(&mut config, &stores);
let core = Core::parse(&mut config, stores, config_manager)
.await
.enable_enterprise();
let data = Data::parse(&mut config);
let cache = Caches::parse(&mut config);
let (ipc, mut ipc_rxs) = build_ipc(&mut config, true);
let inner = Arc::new(Inner {
shared_core: core.into_shared(),
data,
ipc,
cache,
});
// Parse acceptors
servers.parse_tcp_acceptors(&mut config, inner.clone());
// Enable tracing
tracers.enable(true);
// Start services
config.assert_no_errors();
ipc_rxs.spawn_queue_manager(inner.clone());
ipc_rxs.spawn_services(inner.clone());
// Spawn servers
let (shutdown_tx, shutdown_rx) = servers.spawn(|server, acceptor, shutdown_rx| {
match &server.protocol {
ServerProtocol::Smtp | ServerProtocol::Lmtp => server.spawn(
SmtpSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Http => server.spawn(
HttpSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Imap => server.spawn(
ImapSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::Pop3 => server.spawn(
Pop3SessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
ServerProtocol::ManageSieve => server.spawn(
ManageSieveSessionManager::new(inner.clone()),
inner.clone(),
acceptor,
shutdown_rx,
),
};
});
// Start broadcast subscriber
spawn_broadcast_subscriber(inner.clone(), shutdown_rx);
(inner.build_server(), shutdown_tx)
}
const SERVER: &str = r#"
[server]
hostname = "'server{NODE_ID}.example.org'"
http.url = "'https://127.0.0.1:800{NODE_ID}'"
[cluster]
node-id = {NODE_ID}
[server.listener.http]
bind = ["127.0.0.1:1800{NODE_ID}"]
protocol = "http"
max-connections = 81920
tls.implicit = true
[server.listener.imap]
bind = ["127.0.0.1:1900{NODE_ID}"]
protocol = "imap"
max-connections = 81920
[server.listener.lmtp]
bind = ['127.0.0.1:1700{NODE_ID}']
protocol = 'lmtp'
tls.implicit = false
[server.socket]
reuse-addr = true
[server.tls]
enable = true
implicit = false
certificate = "default"
[session.ehlo]
reject-non-fqdn = false
[session.rcpt]
relay = [ { if = "!is_empty(authenticated_as)", then = true },
{ else = false } ]
directory = "'{STORE}'"
[session.rcpt.errors]
total = 5
wait = "1ms"
[session.auth]
mechanisms = "[plain, login, oauthbearer]"
directory = "'{STORE}'"
[resolver]
type = "system"
[queue.outbound]
next-hop = [ { if = "rcpt_domain == 'example.com'", then = "'local'" },
{ if = "contains(['remote.org', 'foobar.com', 'test.com', 'other_domain.com'], rcpt_domain)", then = "'mock-smtp'" },
{ else = false } ]
[store."foundationdb"]
type = "foundationdb"
[store."postgresql"]
type = "postgresql"
host = "localhost"
port = 5432
database = "stalwart"
user = "postgres"
password = "mysecretpassword"
[store."mysql"]
type = "mysql"
host = "localhost"
port = 3307
database = "stalwart"
user = "root"
password = "password"
[certificate.default]
cert = "%{file:{CERT}}%"
private-key = "%{file:{PK}}%"
[storage]
data = "{STORE}"
fts = "{STORE}"
blob = "{STORE}"
lookup = "{STORE}"
directory = "{STORE}"
pubsub = "{PUBSUB}"
[directory."{STORE}"]
type = "internal"
store = "{STORE}"
[imap.auth]
allow-plain-text = true
[oauth]
key = "parerga_und_paralipomena"
[spam-filter]
enable = false
[tracer.console]
type = "console"
level = "{LEVEL}"
multiline = false
ansi = true
disabled-events = ["network.*", "telemetry.webhook-error", "http.request-body",
"eval.result", "store.*", "dkim.*", "queue.*", "delivery.*",
"*.raw-input", "*.raw-output" ]
"#;
const SERVER_NATS: &str = r#"
[store."nats"]
type = "nats"
urls = "127.0.0.1:4444"
"#;
const SERVER_REDIS: &str = r#"
[store."redis"]
type = "redis"
urls = "redis://127.0.0.1"
redis-type = "single"
"#;

View File

@@ -4,10 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{sync::Arc, time::Duration};
use super::assert_is_empty;
use crate::jmap::{mailbox::destroy_all_mailboxes_no_wait, wait_for_index};
use crate::jmap::{assert_is_empty, mailbox::destroy_all_mailboxes_no_wait, wait_for_index};
use common::Server;
use directory::backend::internal::manage::ManageDirectory;
use email::{
@@ -21,6 +18,7 @@ use jmap_client::{
mailbox::{self, Mailbox, Role},
};
use jmap_proto::types::{collection::Collection, id::Id};
use std::{sync::Arc, time::Duration};
use store::{
rand::{self, Rng},
roaring::RoaringBitmap,
@@ -30,7 +28,7 @@ const TEST_USER_ID: u32 = 1;
const NUM_PASSES: usize = 1;
pub async fn test(server: Server, mut client: Client) {
println!("Running concurrency stress tests...");
println!("Running cluster concurrency stress tests...");
server
.core
.storage

View File

@@ -4,13 +4,21 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Duration;
use imap_proto::ResponseType;
use crate::jmap::delivery::SmtpConnection;
use super::{AssertResult, ImapConnection, Type};
pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
const SLEEP: Duration = Duration::from_millis(200);
pub async fn test(
imap: &mut ImapConnection,
imap_check: &mut ImapConnection,
is_cluster_test: bool,
) {
println!("Running IDLE tests...");
// Switch connection to IDLE mode
@@ -28,6 +36,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
// Expect a new mailbox update
imap.send("CREATE Provolone").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -40,6 +51,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
imap.send_untagged(message).await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -53,6 +67,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("STORE 1:* +FLAGS (\\Seen)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -66,6 +83,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
imap.send("CLOSE").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -77,6 +97,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
// Delete folder and expect an update
imap.send("DELETE Provolone").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -88,6 +111,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
imap.send_untagged(message).await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -108,6 +134,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
imap.send("STORE 1 +FLAGS (\\Deleted)").await;
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -118,6 +147,9 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
.await
.assert_contains("* 1 EXPUNGE")
.assert_contains("* 0 EXISTS");
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
@@ -133,7 +165,7 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
.assert_contains("* 0 EXISTS");
// Test SMTP delivery notifications
let mut lmtp = SmtpConnection::connect_port(11201).await;
let mut lmtp = SmtpConnection::connect_port(if is_cluster_test { 17000 } else { 11201 }).await;
lmtp.ingest(
"bill@example.com",
&["jdoe@example.com"],
@@ -148,11 +180,18 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
),
)
.await;
if is_cluster_test {
tokio::time::sleep(SLEEP).await;
}
imap_check
.assert_read(Type::Status, ResponseType::Ok)
.await
.assert_contains("STATUS \"INBOX\"")
.assert_contains("MESSAGES 11");
.assert_contains(if is_cluster_test {
"MESSAGES 1"
} else {
"MESSAGES 11"
});
// Stop IDLE mode
imap_check.send_raw("DONE").await;

View File

@@ -26,6 +26,7 @@ use crate::{
use ::managesieve::core::ManageSieveSessionManager;
use ::store::Stores;
use ahash::AHashSet;
use base64::{Engine, engine::general_purpose};
use common::{
Caches, Core, Data, Inner, Server,
config::{
@@ -95,7 +96,7 @@ pub async fn imap_tests() {
store::test(&mut imap, &mut imap_check, &handle).await;
copy_move::test(&mut imap, &mut imap_check).await;
thread::test(&mut imap, &mut imap_check).await;
idle::test(&mut imap, &mut imap_check).await;
idle::test(&mut imap, &mut imap_check, false).await;
condstore::test(&mut imap, &mut imap_check).await;
acl::test(&mut imap, &mut imap_check).await;
@@ -169,7 +170,7 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest {
let cache = Caches::parse(&mut config);
let store = core.storage.data.clone();
let (ipc, mut ipc_rxs) = build_ipc(&mut config);
let (ipc, mut ipc_rxs) = build_ipc(&mut config, false);
let inner = Arc::new(Inner {
shared_core: core.into_shared(),
data,
@@ -306,8 +307,11 @@ pub enum Type {
impl ImapConnection {
pub async fn connect(tag: &'static [u8]) -> Self {
let (reader, writer) =
tokio::io::split(TcpStream::connect("127.0.0.1:9991").await.unwrap());
Self::connect_to(tag, "127.0.0.1:9991").await
}
pub async fn connect_to(tag: &'static [u8], addr: impl AsRef<str>) -> Self {
let (reader, writer) = tokio::io::split(TcpStream::connect(addr.as_ref()).await.unwrap());
ImapConnection {
tag,
reader: BufReader::new(reader).lines(),
@@ -377,6 +381,16 @@ impl ImapConnection {
}
}
pub async fn authenticate(&mut self, user: &str, pass: &str) {
let creds = general_purpose::STANDARD.encode(format!("\0{user}\0{pass}"));
self.send(&format!(
"AUTHENTICATE PLAIN {{{}+}}\r\n{creds}",
creds.len()
))
.await;
self.assert_read(Type::Tagged, ResponseType::Ok).await;
}
pub async fn send(&mut self, text: &str) {
//let c = println!("-> {}{:?}", std::str::from_utf8(self.tag).unwrap(), text);
self.writer.write_all(self.tag).await.unwrap();

View File

@@ -186,7 +186,7 @@ impl EnterpriseCore for Core {
async fn alerts(server: &Server) {
// Make sure the required metrics are set to 0
assert_eq!(
Collector::read_event_metric(EventType::Cluster(ClusterEvent::Error).id()),
Collector::read_event_metric(EventType::Cluster(ClusterEvent::PublisherError).id()),
0
);
assert_eq!(Collector::read_metric(MetricType::DomainCount), 0.0);
@@ -196,12 +196,12 @@ async fn alerts(server: &Server) {
);
// Increment metrics to trigger alerts
Collector::update_event_counter(EventType::Cluster(ClusterEvent::Error), 5);
Collector::update_event_counter(EventType::Cluster(ClusterEvent::PublisherError), 5);
Collector::update_gauge(MetricType::DomainCount, 3);
// Make sure the values were set
assert_eq!(
Collector::read_event_metric(EventType::Cluster(ClusterEvent::Error).id()),
Collector::read_event_metric(EventType::Cluster(ClusterEvent::PublisherError).id()),
5
);
assert_eq!(Collector::read_metric(MetricType::DomainCount), 3.0);

View File

@@ -73,7 +73,6 @@ pub mod purge;
pub mod push_subscription;
pub mod quota;
pub mod sieve_script;
pub mod stress_test;
pub mod thread_get;
pub mod thread_merge;
pub mod vacation_response;
@@ -135,19 +134,6 @@ async fn jmap_tests_() {
}
}
#[tokio::test(flavor = "multi_thread")]
#[ignore]
pub async fn jmap_stress_tests() {
let params = init_jmap_tests(
&std::env::var("STORE")
.expect("Missing store type. Try running `STORE=<store_type> cargo test`"),
true,
)
.await;
stress_test::test(params.server.clone(), params.client).await;
params.temp_dir.delete();
}
#[ignore]
#[tokio::test(flavor = "multi_thread")]
pub async fn jmap_metric_tests() {
@@ -328,7 +314,7 @@ async fn init_jmap_tests(store_id: &str, delete_if_exists: bool) -> JMAPTest {
let data = Data::parse(&mut config);
let cache = Caches::parse(&mut config);
let store = core.storage.data.clone();
let (ipc, mut ipc_rxs) = build_ipc(&mut config);
let (ipc, mut ipc_rxs) = build_ipc(&mut config, false);
let inner = Arc::new(Inner {
shared_core: core.into_shared(),
data,

View File

@@ -15,6 +15,8 @@ use trc::Collector;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
#[cfg(test)]
pub mod cluster;
#[cfg(test)]
pub mod directory;
#[cfg(test)]
@@ -76,3 +78,16 @@ pub fn enable_logging() {
}
}
}
pub const TEST_USERS: &[(&str, &str, &str, &str)] = &[
("admin", "secret1", "Superuser", "admin@example,com"),
("john", "secret2", "John Doe", "jdoe@example.com"),
(
"jane",
"secret3",
"Jane Doe-Smith",
"jane.smith@example.com",
),
("bill", "secret4", "Bill Foobar", "bill@example,com"),
("mike", "secret5", "Mike Noquota", "mike@example,com"),
];

View File

@@ -173,7 +173,7 @@ impl TestSMTP {
}
pub fn inner_with_rxs(&self) -> (Arc<Inner>, IpcReceivers) {
let (ipc, ipc_rxs) = build_ipc(&mut Config::default());
let (ipc, ipc_rxs) = build_ipc(&mut Config::default(), false);
(
Inner {
@@ -191,7 +191,7 @@ impl TestSMTP {
let store = core.storage.data.clone();
let blob_store = core.storage.blob.clone();
let shared_core = core.into_shared();
let (ipc, mut ipc_rxs) = build_ipc(&mut Config::default());
let (ipc, mut ipc_rxs) = build_ipc(&mut Config::default(), false);
TestSMTP {
queue_receiver: QueueReceiver {

View File

@@ -5,7 +5,7 @@
*/
use crate::{
AssertConfig, add_test_certs, directory::internal::TestInternalDirectory,
AssertConfig, TEST_USERS, add_test_certs, directory::internal::TestInternalDirectory,
jmap::assert_is_empty, store::TempDir,
};
use ::managesieve::core::ManageSieveSessionManager;
@@ -133,7 +133,7 @@ async fn init_webdav_tests(store_id: &str, delete_if_exists: bool) -> WebDavTest
let cache = Caches::parse(&mut config);
let store = core.storage.data.clone();
let (ipc, mut ipc_rxs) = build_ipc(&mut config);
let (ipc, mut ipc_rxs) = build_ipc(&mut config, false);
let inner = Arc::new(Inner {
shared_core: core.into_shared(),
data,
@@ -194,7 +194,7 @@ async fn init_webdav_tests(store_id: &str, delete_if_exists: bool) -> WebDavTest
// Create test accounts
let mut clients = AHashMap::new();
for (account, secret, name, email) in TEST_DAV_USERS {
for (account, secret, name, email) in TEST_USERS {
let account_id = store
.create_test_user(account, secret, name, &[email])
.await;
@@ -1127,16 +1127,3 @@ ansi = true
disabled-events = ["network.*"]
"#;
pub const TEST_DAV_USERS: &[(&str, &str, &str, &str)] = &[
("admin", "secret1", "Superuser", "admin@example,com"),
("john", "secret2", "John Doe", "jdoe@example.com"),
(
"jane",
"secret3",
"Jane Doe-Smith",
"jane.smith@example.com",
),
("bill", "secret4", "Bill Foobar", "bill@example,com"),
("mike", "secret5", "Mike Noquota", "mike@example,com"),
];

View File

@@ -5,7 +5,7 @@
*/
use super::WebDavTest;
use crate::webdav::{TEST_DAV_USERS, prop::ALL_DAV_PROPERTIES};
use crate::{TEST_USERS, webdav::prop::ALL_DAV_PROPERTIES};
use dav_proto::schema::property::{DavProperty, PrincipalProperty, WebDavProperty};
use groupware::DavResourceName;
use hyper::StatusCode;
@@ -23,7 +23,7 @@ pub async fn test(test: &WebDavTest) {
ALL_DAV_PROPERTIES,
)
.await;
for (account, _, name, _) in TEST_DAV_USERS {
for (account, _, name, _) in TEST_USERS {
let props = response.properties(&format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
@@ -174,7 +174,7 @@ pub async fn test(test: &WebDavTest) {
.with_values([format!("D:href:{}/jane/", DavResourceName::Card.base_path()).as_str()])
.with_status(StatusCode::OK);
for (account, _, name, _) in TEST_DAV_USERS
for (account, _, name, _) in TEST_USERS
.iter()
.filter(|(account, _, _, _)| ["jane", "support"].contains(account))
{
@@ -310,7 +310,7 @@ pub async fn test(test: &WebDavTest) {
response
.properties(&format!("{}/jane/", DavResourceName::Principal.base_path()))
.get(DavProperty::WebDav(WebDavProperty::DisplayName))
.with_values([TEST_DAV_USERS
.with_values([TEST_USERS
.iter()
.find(|(account, _, _, _)| *account == "jane")
.unwrap()