Registry testing - all tests passing
This commit is contained in:
197
tests/src/directory/integration.rs
Normal file
197
tests/src/directory/integration.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
directory::ldap::ldap_test_directory,
|
||||
utils::{server::TestServerBuilder, smtp::SmtpConnection},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use email::cache::MessageCacheFetch;
|
||||
use registry::schema::structs::{Account, AccountSettings, Directory};
|
||||
use types::id::Id;
|
||||
|
||||
pub async fn test() {
|
||||
let test = TestServerBuilder::new("directory_integration_test")
|
||||
.await
|
||||
.with_default_listeners()
|
||||
.await
|
||||
.with_object(Directory::Ldap(ldap_test_directory()))
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
let admin = test.account("admin");
|
||||
admin.mta_no_auth().await;
|
||||
admin.mta_disable_spam_filter().await;
|
||||
admin.reload_settings().await;
|
||||
|
||||
// Test account creation by login
|
||||
let account = crate::utils::account::Account::new(
|
||||
"john.doe@example.org",
|
||||
"this is John's LDAP password",
|
||||
&[],
|
||||
"",
|
||||
Id::from(u32::MAX),
|
||||
);
|
||||
assert_eq!(
|
||||
account
|
||||
.registry_get::<AccountSettings>(Id::singleton())
|
||||
.await
|
||||
.description
|
||||
.as_deref(),
|
||||
Some("John Doe")
|
||||
);
|
||||
|
||||
// Test account creation by rcpt
|
||||
let mut lmtp = SmtpConnection::connect().await;
|
||||
for rcpt in [
|
||||
"corporate@example.org",
|
||||
"jane.smith@example.org",
|
||||
"john@example.org",
|
||||
"bill@example.org",
|
||||
"sales@example.org",
|
||||
] {
|
||||
lmtp.ingest(
|
||||
"bill@remote.org",
|
||||
&[rcpt],
|
||||
&TEST_EMAIL.replace("$RCPT", rcpt),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Fetch all accounts
|
||||
let mut accounts = admin
|
||||
.registry_get_all::<Account>()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(id, account)| {
|
||||
(
|
||||
match &account {
|
||||
Account::User(user_account) => user_account.name.clone(),
|
||||
Account::Group(group_account) => group_account.name.clone(),
|
||||
},
|
||||
(account, id),
|
||||
)
|
||||
})
|
||||
.collect::<AHashMap<_, _>>();
|
||||
assert_eq!(accounts.len(), 5, "Got: {accounts:#?}");
|
||||
|
||||
// Validate accounts
|
||||
for (name, description, secret, groups, aliases) in [
|
||||
(
|
||||
"john.doe",
|
||||
"John Doe",
|
||||
"$app$8958830913002348890$",
|
||||
&["sales"][..],
|
||||
&["john"][..],
|
||||
),
|
||||
(
|
||||
"jane.smith",
|
||||
"Jane Smith",
|
||||
"$app$4096614298472586996$",
|
||||
&["sales", "corporate"][..],
|
||||
&[][..],
|
||||
),
|
||||
(
|
||||
"bill.foobar",
|
||||
"Bill Foobar",
|
||||
"",
|
||||
&["corporate"][..],
|
||||
&["bill"][..],
|
||||
),
|
||||
] {
|
||||
let (account, id) = accounts
|
||||
.remove(name)
|
||||
.map(|(account, id)| (account.into_user().unwrap(), id))
|
||||
.unwrap();
|
||||
assert_eq!(account.description.as_deref(), Some(description));
|
||||
if !secret.is_empty() {
|
||||
assert_eq!(
|
||||
test.server
|
||||
.registry()
|
||||
.object::<Account>(id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_user()
|
||||
.unwrap()
|
||||
.credentials
|
||||
.values()
|
||||
.next()
|
||||
.and_then(|v| v.as_main_credential())
|
||||
.map(|v| v.secret.as_str()),
|
||||
Some(secret)
|
||||
);
|
||||
}
|
||||
for group in groups {
|
||||
let id = accounts.get(*group).unwrap().1;
|
||||
assert!(
|
||||
account
|
||||
.member_group_ids
|
||||
.iter()
|
||||
.any(|group_id| group_id == &id),
|
||||
"Account {name} is not a member of group {group}"
|
||||
);
|
||||
}
|
||||
for alias in aliases {
|
||||
assert!(
|
||||
account
|
||||
.aliases
|
||||
.iter()
|
||||
.any(|account_alias| account_alias.name == *alias),
|
||||
"Account {name} does not have alias {alias}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
test.server
|
||||
.get_cached_messages(id.document_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.emails
|
||||
.index
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// Validate groups
|
||||
for (name, description, aliases) in [
|
||||
("sales", "sales", &[][..]),
|
||||
("corporate", "corporate", &["everyone"][..]),
|
||||
] {
|
||||
let (account, id) = accounts
|
||||
.remove(name)
|
||||
.map(|(account, id)| (account.into_group().unwrap(), id))
|
||||
.unwrap();
|
||||
assert_eq!(account.description.as_deref(), Some(description));
|
||||
for alias in aliases {
|
||||
assert!(
|
||||
account
|
||||
.aliases
|
||||
.iter()
|
||||
.any(|account_alias| account_alias.name == *alias),
|
||||
"Group {name} does not have alias {alias}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
test.server
|
||||
.get_cached_messages(id.document_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.emails
|
||||
.index
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_EMAIL: &str = r#"From: bill@remote.org
|
||||
To: $RCPT
|
||||
Subject: TPS Report for $RCPT
|
||||
|
||||
I'm going to need those TPS reports ASAP. So, if you could do that, that'd be great.
|
||||
|
||||
"#;
|
||||
@@ -10,34 +10,8 @@ use registry::{
|
||||
types::map::Map,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn ldap_directory() {
|
||||
let mut config = structs::LdapDirectory {
|
||||
url: "ldap://localhost".into(),
|
||||
use_tls: false,
|
||||
attr_class: Map::new(vec!["objectClass".to_string()]),
|
||||
attr_description: Map::new(vec!["cn".to_string()]),
|
||||
attr_email: Map::new(vec!["mail".to_string()]),
|
||||
attr_email_alias: Map::new(vec!["mailAlias".to_string()]),
|
||||
attr_member_of: Map::new(vec!["memberOf".to_string()]),
|
||||
attr_secret: Map::new(vec![]),
|
||||
attr_secret_changed: Map::new(vec!["shadowLastChange".to_string()]),
|
||||
base_dn: "dc=stalwart,dc=test".into(),
|
||||
bind_dn: "cn=admin,dc=stalwart,dc=test".to_string().into(),
|
||||
bind_secret: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "admin".into(),
|
||||
}),
|
||||
filter_member_of: "(&(objectClass=groupOfNames)(member=?))".to_string().into(),
|
||||
filter_login: "(&(objectClass=inetOrgPerson)(mail=?))".into(),
|
||||
filter_mailbox: concat!(
|
||||
"(|(&(objectClass=inetOrgPerson)(|(mail=?)(mailAlias=?)))",
|
||||
"(&(objectClass=groupOfNames)(|(mail=?)(mailAlias=?))))"
|
||||
)
|
||||
.into(),
|
||||
group_class: "groupOfNames".into(),
|
||||
bind_authentication: true,
|
||||
..Default::default()
|
||||
};
|
||||
pub async fn test() {
|
||||
let mut config = ldap_test_directory();
|
||||
|
||||
// Test bind authentication
|
||||
let ldap = LdapDirectory::open(config.clone()).await.unwrap();
|
||||
@@ -156,3 +130,32 @@ async fn ldap_directory() {
|
||||
Recipient::Invalid
|
||||
);
|
||||
}
|
||||
|
||||
pub fn ldap_test_directory() -> structs::LdapDirectory {
|
||||
structs::LdapDirectory {
|
||||
url: "ldap://localhost".into(),
|
||||
use_tls: false,
|
||||
attr_class: Map::new(vec!["objectClass".to_string()]),
|
||||
attr_description: Map::new(vec!["cn".to_string()]),
|
||||
attr_email: Map::new(vec!["mail".to_string()]),
|
||||
attr_email_alias: Map::new(vec!["mailAlias".to_string()]),
|
||||
attr_member_of: Map::new(vec!["memberOf".to_string()]),
|
||||
attr_secret: Map::new(vec![]),
|
||||
attr_secret_changed: Map::new(vec!["shadowLastChange".to_string()]),
|
||||
base_dn: "dc=stalwart,dc=test".into(),
|
||||
bind_dn: "cn=admin,dc=stalwart,dc=test".to_string().into(),
|
||||
bind_secret: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "admin".into(),
|
||||
}),
|
||||
filter_member_of: "(&(objectClass=groupOfNames)(member=?))".to_string().into(),
|
||||
filter_login: "(&(objectClass=inetOrgPerson)(mail=?))".into(),
|
||||
filter_mailbox: concat!(
|
||||
"(|(&(objectClass=inetOrgPerson)(|(mail=?)(mailAlias=?)))",
|
||||
"(&(objectClass=groupOfNames)(|(mail=?)(mailAlias=?))))"
|
||||
)
|
||||
.into(),
|
||||
group_class: "groupOfNames".into(),
|
||||
bind_authentication: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod integration;
|
||||
pub mod ldap;
|
||||
pub mod oidc;
|
||||
//pub mod sql;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub mod sql;
|
||||
pub mod synchronization;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
pub async fn directory_tests() {
|
||||
ldap::test().await;
|
||||
oidc::test().await;
|
||||
#[cfg(feature = "sqlite")]
|
||||
sql::test().await;
|
||||
synchronization::test().await;
|
||||
integration::test().await;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
use directory::{Account, Credentials, Directory, backend::oidc::OpenIdDirectory};
|
||||
use registry::{schema::structs, types::map::Map};
|
||||
|
||||
#[tokio::test]
|
||||
async fn oidc_directory() {
|
||||
pub async fn test() {
|
||||
let config = structs::OidcDirectory {
|
||||
issuer_url: "http://localhost:9080/realms/stalwart".to_string(),
|
||||
claim_username: "preferred_username".to_string(),
|
||||
|
||||
@@ -4,573 +4,141 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use mail_send::Credentials;
|
||||
use directory::{Account, Credentials, Group, Recipient, backend::sql::SqlDirectory};
|
||||
use registry::schema::structs::{self, SqlAuthStore};
|
||||
use store::{Store, backend::sqlite::SqliteStore};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use store::{InMemoryStore, Store};
|
||||
pub async fn test() {
|
||||
let sql_store = Store::SQLite(SqliteStore::open_memory().unwrap().into());
|
||||
|
||||
use crate::{
|
||||
directory::{DirectoryTest, IntoTestPrincipal, TestPrincipal, map_account_id, map_account_ids},
|
||||
store::cleanup::store_destroy,
|
||||
};
|
||||
|
||||
use super::DirectoryStore;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sql_directory() {
|
||||
// Enable logging
|
||||
/*tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::TRACE)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();*/
|
||||
|
||||
// Obtain directory handle
|
||||
for directory_id in ["sqlite", "postgresql", "mysql"] {
|
||||
// Parse config
|
||||
let mut config = DirectoryTest::new(directory_id.into()).await;
|
||||
|
||||
println!("Testing SQL directory {:?}", directory_id);
|
||||
let handle = config.directories.directories.remove(directory_id).unwrap();
|
||||
let store = DirectoryStore {
|
||||
store: config.stores.stores.remove(directory_id).unwrap(),
|
||||
};
|
||||
let base_store = &store.store;
|
||||
let core = config.server;
|
||||
|
||||
// Create tables
|
||||
store_destroy(base_store).await;
|
||||
store.create_test_directory().await;
|
||||
|
||||
// Create test users
|
||||
store
|
||||
.create_test_user("admin", "very_secret", "Administrator")
|
||||
.await;
|
||||
store.create_test_user("john", "12345", "John Doe").await;
|
||||
store.create_test_user("jane", "abcde", "Jane Doe").await;
|
||||
store
|
||||
.create_test_user(
|
||||
"bill",
|
||||
"$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe",
|
||||
"Bill Foobar",
|
||||
)
|
||||
.await;
|
||||
store.set_test_quota("bill", 500000).await;
|
||||
|
||||
// Create test groups
|
||||
store.create_test_group("sales", "Sales Team").await;
|
||||
store.create_test_group("support", "Support Team").await;
|
||||
|
||||
// Link users to groups
|
||||
store.add_to_group("john", "sales").await;
|
||||
store.add_to_group("jane", "sales").await;
|
||||
store.add_to_group("jane", "support").await;
|
||||
|
||||
// Add email addresses
|
||||
store
|
||||
.link_test_address("john", "john@example.org", "primary")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("jane", "jane@example.org", "primary")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("bill", "bill@example.org", "primary")
|
||||
.await;
|
||||
|
||||
// Add aliases and lists
|
||||
store
|
||||
.link_test_address("john", "john.doe@example.org", "alias")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("john", "jdoe@example.org", "alias")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("john", "info@example.org", "list")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("jane", "info@example.org", "list")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("bill", "info@example.org", "list")
|
||||
.await;
|
||||
|
||||
// Add catch-all user
|
||||
store
|
||||
.create_test_user("robert", "abcde", "Robert Foobar")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("robert", "robert@catchall.org", "primary")
|
||||
.await;
|
||||
store
|
||||
.link_test_address("robert", "@catchall.org", "alias")
|
||||
.await;
|
||||
|
||||
// Test authentication
|
||||
assert_eq!(
|
||||
handle
|
||||
.query(
|
||||
QueryParams::credentials(&Credentials::Plain {
|
||||
username: "john".into(),
|
||||
secret: "12345".into()
|
||||
})
|
||||
.with_return_member_of(true)
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_test(),
|
||||
TestPrincipal {
|
||||
id: base_store.get_principal_id("john").await.unwrap().unwrap(),
|
||||
name: "john".into(),
|
||||
description: Some("John Doe".into()),
|
||||
secrets: vec!["12345".into()],
|
||||
typ: Type::Individual,
|
||||
member_of: map_account_ids(base_store, vec!["sales"])
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect(),
|
||||
emails: vec![
|
||||
"john@example.org".into(),
|
||||
"jdoe@example.org".into(),
|
||||
"john.doe@example.org".into()
|
||||
],
|
||||
roles: vec![ROLE_USER.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
handle
|
||||
.query(
|
||||
QueryParams::credentials(&Credentials::Plain {
|
||||
username: "bill".into(),
|
||||
secret: "password".into()
|
||||
})
|
||||
.with_return_member_of(true)
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_test(),
|
||||
TestPrincipal {
|
||||
id: base_store.get_principal_id("bill").await.unwrap().unwrap(),
|
||||
name: "bill".into(),
|
||||
description: Some("Bill Foobar".into()),
|
||||
secrets: vec![
|
||||
"$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".into()
|
||||
],
|
||||
typ: Type::Individual,
|
||||
quota: 500000,
|
||||
emails: vec!["bill@example.org".into(),],
|
||||
roles: vec![ROLE_USER.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
handle
|
||||
.query(
|
||||
QueryParams::credentials(&Credentials::Plain {
|
||||
username: "admin".into(),
|
||||
secret: "very_secret".into()
|
||||
})
|
||||
.with_return_member_of(true)
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_test(),
|
||||
TestPrincipal {
|
||||
id: base_store.get_principal_id("admin").await.unwrap().unwrap(),
|
||||
name: "admin".into(),
|
||||
description: Some("Administrator".into()),
|
||||
secrets: vec!["very_secret".into()],
|
||||
typ: Type::Individual,
|
||||
roles: vec![ROLE_ADMIN.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
handle
|
||||
.query(
|
||||
QueryParams::credentials(&Credentials::Plain {
|
||||
username: "bill".into(),
|
||||
secret: "invalid".into()
|
||||
})
|
||||
.with_return_member_of(true)
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// Get user by name
|
||||
let mut p = handle
|
||||
.query(QueryParams::name("jane").with_return_member_of(true))
|
||||
// Create test directory
|
||||
for query in [
|
||||
concat!(
|
||||
"CREATE TABLE accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT,",
|
||||
" type TEXT NOT NULL, active BOOLEAN DEFAULT TRUE)"
|
||||
),
|
||||
concat!(
|
||||
"CREATE TABLE group_members (name TEXT NOT NULL, member_of ",
|
||||
"TEXT NOT NULL, PRIMARY KEY (name, member_of))"
|
||||
),
|
||||
concat!(
|
||||
"CREATE TABLE emails (name TEXT NOT NULL, address TEXT NOT",
|
||||
" NULL, PRIMARY KEY (name, address))"
|
||||
),
|
||||
concat!(
|
||||
"INSERT INTO accounts (name, secret, description, type) ",
|
||||
"VALUES ('john@example.org', 'john secret', 'John Doe', 'individual')"
|
||||
),
|
||||
concat!(
|
||||
"INSERT INTO accounts (name, secret, description, type) ",
|
||||
"VALUES ('jane@example.org', 'jane secret', 'Jane Doe', 'individual')"
|
||||
),
|
||||
concat!(
|
||||
"INSERT INTO accounts (name, secret, description, type) ",
|
||||
"VALUES ('sales@example.org', NULL, 'Sales Team', 'group')"
|
||||
),
|
||||
concat!(
|
||||
"INSERT INTO group_members (name, member_of) VALUES ",
|
||||
"('john@example.org', 'sales@example.org')"
|
||||
),
|
||||
concat!(
|
||||
"INSERT INTO group_members (name, member_of) VALUES ",
|
||||
"('jane@example.org', 'sales@example.org')"
|
||||
),
|
||||
concat!(
|
||||
"INSERT INTO emails (name, address) VALUES ",
|
||||
"('john@example.org', 'john.doe@example.org')"
|
||||
),
|
||||
] {
|
||||
sql_store
|
||||
.sql_query::<usize>(query, vec![])
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_test();
|
||||
p.member_of.sort();
|
||||
assert_eq!(
|
||||
p,
|
||||
TestPrincipal {
|
||||
id: base_store.get_principal_id("jane").await.unwrap().unwrap(),
|
||||
name: "jane".into(),
|
||||
description: Some("Jane Doe".into()),
|
||||
typ: Type::Individual,
|
||||
secrets: vec!["abcde".into()],
|
||||
member_of: map_account_ids(base_store, vec!["sales", "support"])
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect(),
|
||||
emails: vec!["jane@example.org".into(),],
|
||||
roles: vec![ROLE_USER.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// Get group by name
|
||||
assert_eq!(
|
||||
handle
|
||||
.query(QueryParams::name("sales").with_return_member_of(true))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_test(),
|
||||
TestPrincipal {
|
||||
id: base_store.get_principal_id("sales").await.unwrap().unwrap(),
|
||||
name: "sales".into(),
|
||||
description: Some("Sales Team".into()),
|
||||
typ: Type::Group,
|
||||
roles: vec![ROLE_USER.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// Ids by email
|
||||
assert_eq!(
|
||||
core.email_to_id(&handle, "jane@example.org", 0)
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(map_account_id(base_store, "jane").await)
|
||||
);
|
||||
assert_eq!(
|
||||
core.email_to_id(&handle, "jane+alias@example.org", 0)
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(map_account_id(base_store, "jane").await)
|
||||
);
|
||||
assert_eq!(
|
||||
core.email_to_id(&handle, "unknown@example.org", 0)
|
||||
.await
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
core.email_to_id(&handle, "anything@catchall.org", 0)
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(map_account_id(base_store, "robert").await)
|
||||
);
|
||||
|
||||
// Domain validation
|
||||
assert!(handle.is_local_domain("example.org").await.unwrap());
|
||||
assert!(!handle.is_local_domain("other.org").await.unwrap());
|
||||
|
||||
// RCPT TO
|
||||
assert_eq!(
|
||||
core.rcpt(&handle, "jane@example.org", 0).await.unwrap(),
|
||||
RcptType::Mailbox
|
||||
);
|
||||
assert_eq!(
|
||||
core.rcpt(&handle, "info@example.org", 0).await.unwrap(),
|
||||
RcptType::Mailbox
|
||||
);
|
||||
assert_eq!(
|
||||
core.rcpt(&handle, "jane+alias@example.org", 0)
|
||||
.await
|
||||
.unwrap(),
|
||||
RcptType::Mailbox
|
||||
);
|
||||
assert_eq!(
|
||||
core.rcpt(&handle, "info+alias@example.org", 0)
|
||||
.await
|
||||
.unwrap(),
|
||||
RcptType::Mailbox
|
||||
);
|
||||
assert_eq!(
|
||||
core.rcpt(&handle, "random_user@catchall.org", 0)
|
||||
.await
|
||||
.unwrap(),
|
||||
RcptType::Mailbox
|
||||
);
|
||||
assert_eq!(
|
||||
core.rcpt(&handle, "invalid@example.org", 0).await.unwrap(),
|
||||
RcptType::Invalid
|
||||
);
|
||||
|
||||
// VRFY
|
||||
assert_eq!(
|
||||
core.vrfy(&handle, "jane", 0).await.unwrap(),
|
||||
vec!["jane@example.org".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
core.vrfy(&handle, "john", 0).await.unwrap(),
|
||||
vec![
|
||||
"john.doe@example.org".to_string(),
|
||||
"john@example.org".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
core.vrfy(&handle, "jane+alias@example", 0).await.unwrap(),
|
||||
vec!["jane@example.org".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
core.vrfy(&handle, "info", 0).await.unwrap(),
|
||||
Vec::<String>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
core.vrfy(&handle, "invalid", 0).await.unwrap(),
|
||||
Vec::<String>::new()
|
||||
);
|
||||
|
||||
// EXPN (now handled by the internal store)
|
||||
/*assert_eq!(
|
||||
core.expn(&handle, "info@example.org", 0).await.unwrap(),
|
||||
vec![
|
||||
"bill@example.org".into(),
|
||||
"jane@example.org".into(),
|
||||
"john@example.org".into()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
core.expn(&handle, "john@example.org", 0).await.unwrap(),
|
||||
Vec::<String>::new()
|
||||
);*/
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryStore {
|
||||
pub async fn create_test_directory(&self) {
|
||||
// Create tables
|
||||
for table in ["accounts", "group_members", "emails"] {
|
||||
self.store
|
||||
.sql_query::<usize>(&format!("DROP TABLE IF EXISTS {table}"), vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
for query in [
|
||||
concat!(
|
||||
"CREATE TABLE accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT,",
|
||||
" type TEXT NOT NULL, quota INTEGER ",
|
||||
"DEFAULT 0, active BOOLEAN DEFAULT TRUE)"
|
||||
),
|
||||
concat!(
|
||||
"CREATE TABLE group_members (name TEXT NOT NULL, member_of ",
|
||||
"TEXT NOT NULL, PRIMARY KEY (name, member_of))"
|
||||
),
|
||||
concat!(
|
||||
"CREATE TABLE emails (name TEXT NOT NULL, address TEXT NOT",
|
||||
" NULL, type TEXT, PRIMARY KEY (name, address))"
|
||||
),
|
||||
"INSERT INTO accounts (name, secret, type) VALUES ('admin', 'secret', 'admin')",
|
||||
] {
|
||||
let query = if self.is_mysql() {
|
||||
query.replace("TEXT", "VARCHAR(255)")
|
||||
} else {
|
||||
query.into()
|
||||
};
|
||||
|
||||
self.store
|
||||
.sql_query::<usize>(&query, vec![])
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("failed for {query}"));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_test_user(&self, login: &str, secret: &str, name: &str) {
|
||||
let account_type = if login == "admin" {
|
||||
"admin"
|
||||
} else {
|
||||
"individual"
|
||||
};
|
||||
self.store
|
||||
.sql_query::<usize>(
|
||||
if self.is_postgresql() {
|
||||
concat!(
|
||||
"INSERT INTO accounts (name, secret, description, ",
|
||||
"type, active) VALUES ($1, $2, $3, $4, true) ",
|
||||
"ON CONFLICT (name) ",
|
||||
"DO UPDATE SET secret = $2, description = $3, type = $4, active = true"
|
||||
)
|
||||
} else if self.is_mysql() {
|
||||
concat!(
|
||||
"INSERT INTO accounts (name, secret, description, ",
|
||||
"type, active) VALUES (?, ?, ?, ?, true) ",
|
||||
"ON DUPLICATE KEY UPDATE ",
|
||||
"secret = VALUES(secret), description = VALUES(description), ",
|
||||
"type = VALUES(type), active = true"
|
||||
)
|
||||
} else {
|
||||
concat!(
|
||||
"INSERT INTO accounts (name, secret, description, ",
|
||||
"type, active) VALUES (?, ?, ?, ?, true) ",
|
||||
"ON CONFLICT(name) DO UPDATE SET ",
|
||||
"secret = excluded.secret, description = excluded.description, ",
|
||||
"type = excluded.type, active = true"
|
||||
)
|
||||
},
|
||||
vec![
|
||||
login.into(),
|
||||
secret.into(),
|
||||
name.into(),
|
||||
account_type.into(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn create_test_user_with_email(&self, login: &str, secret: &str, name: &str) {
|
||||
self.create_test_user(login, secret, name).await;
|
||||
self.link_test_address(login, login, "primary").await;
|
||||
}
|
||||
|
||||
pub async fn create_test_group(&self, login: &str, name: &str) {
|
||||
self.store
|
||||
.sql_query::<usize>(
|
||||
if self.is_postgresql() {
|
||||
concat!(
|
||||
"INSERT INTO accounts (name, description, ",
|
||||
"type, active) VALUES ($1, $2, $3, $4) ON CONFLICT (name) DO NOTHING"
|
||||
)
|
||||
} else if self.is_mysql() {
|
||||
concat!(
|
||||
"INSERT IGNORE INTO accounts (name, description, ",
|
||||
"type, active) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
} else {
|
||||
concat!(
|
||||
"INSERT OR IGNORE INTO accounts (name, description, ",
|
||||
"type, active) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
},
|
||||
vec![login.into(), name.into(), "group".into(), true.into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn create_test_group_with_email(&self, login: &str, name: &str) {
|
||||
self.create_test_group(login, name).await;
|
||||
self.link_test_address(login, login, "primary").await;
|
||||
}
|
||||
|
||||
pub async fn link_test_address(&self, login: &str, address: &str, typ: &str) {
|
||||
self.store
|
||||
.sql_query::<usize>(
|
||||
if self.is_postgresql() {
|
||||
"INSERT INTO emails (name, address, type) VALUES ($1, $2, $3) ON CONFLICT (name, address) DO NOTHING"
|
||||
} else if self.is_mysql() {
|
||||
"INSERT IGNORE INTO emails (name, address, type) VALUES (?, ?, ?)"
|
||||
} else {
|
||||
"INSERT OR IGNORE INTO emails (name, address, type) VALUES (?, ?, ?)"
|
||||
},
|
||||
vec![login.into(), address.into(), typ.into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn set_test_quota(&self, login: &str, quota: u32) {
|
||||
self.store
|
||||
.sql_query::<usize>(
|
||||
if self.is_postgresql() {
|
||||
"UPDATE accounts SET quota = $1 where name = $2"
|
||||
} else {
|
||||
"UPDATE accounts SET quota = ? where name = ?"
|
||||
},
|
||||
vec![quota.into(), login.into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn add_to_group(&self, login: &str, group: &str) {
|
||||
self.store
|
||||
.sql_query::<usize>(
|
||||
if self.is_postgresql() {
|
||||
"INSERT INTO group_members (name, member_of) VALUES ($1, $2)"
|
||||
} else {
|
||||
"INSERT INTO group_members (name, member_of) VALUES (?, ?)"
|
||||
},
|
||||
vec![login.into(), group.into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn remove_from_group(&self, login: &str, group: &str) {
|
||||
self.store
|
||||
.sql_query::<usize>(
|
||||
if self.is_postgresql() {
|
||||
"DELETE FROM group_members WHERE name = $1 AND member_of = $2"
|
||||
} else {
|
||||
"DELETE FROM group_members WHERE name = ? AND member_of = ?"
|
||||
},
|
||||
vec![login.into(), group.into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn remove_test_alias(&self, login: &str, alias: &str) {
|
||||
self.store
|
||||
.sql_query::<usize>(
|
||||
if self.is_postgresql() {
|
||||
"DELETE FROM emails WHERE name = $1 AND address = $2"
|
||||
} else {
|
||||
"DELETE FROM emails WHERE name = ? AND address = ?"
|
||||
},
|
||||
vec![login.into(), alias.into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn is_mysql(&self) -> bool {
|
||||
#[cfg(feature = "mysql")]
|
||||
{
|
||||
matches!(self.store, Store::MySQL(_))
|
||||
}
|
||||
#[cfg(not(feature = "mysql"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn is_postgresql(&self) -> bool {
|
||||
#[cfg(feature = "postgres")]
|
||||
{
|
||||
matches!(self.store, Store::PostgreSQL(_))
|
||||
}
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn is_sqlite(&self) -> bool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
{
|
||||
matches!(self.store, Store::SQLite(_))
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
.unwrap_or_else(|_| panic!("failed for {query}"));
|
||||
}
|
||||
|
||||
let config = structs::SqlDirectory {
|
||||
query_login: concat!(
|
||||
"SELECT name, secret, description, type FROM accounts ",
|
||||
"WHERE name = $1 AND active = true"
|
||||
)
|
||||
.into(),
|
||||
query_recipient: concat!(
|
||||
"SELECT name, secret, description, type FROM accounts ",
|
||||
"WHERE name = $1 AND active = true"
|
||||
)
|
||||
.into(),
|
||||
query_email_aliases: concat!("SELECT address FROM emails ", "WHERE name = $1")
|
||||
.to_string()
|
||||
.into(),
|
||||
query_member_of: concat!("SELECT member_of FROM group_members ", "WHERE name = $1")
|
||||
.to_string()
|
||||
.into(),
|
||||
column_class: "type".to_string().into(),
|
||||
column_description: "description".to_string().into(),
|
||||
column_email: "name".into(),
|
||||
column_secret: "secret".into(),
|
||||
store: SqlAuthStore::Default,
|
||||
};
|
||||
|
||||
// Test authentication
|
||||
let sql = SqlDirectory::open(config, &sql_store).await.unwrap();
|
||||
assert_eq!(
|
||||
sql.authenticate(&Credentials::Basic {
|
||||
username: "john@example.org".to_string(),
|
||||
secret: "john secret".to_string(),
|
||||
mfa_token: None,
|
||||
})
|
||||
.await
|
||||
.unwrap(),
|
||||
Account {
|
||||
email: "john@example.org".to_string(),
|
||||
email_aliases: vec!["john.doe@example.org".to_string(),],
|
||||
secret: Some("john secret".to_string()),
|
||||
groups: vec!["sales@example.org".to_string()],
|
||||
description: Some("John Doe".to_string()),
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
sql.authenticate(&Credentials::Basic {
|
||||
username: "john@example.org".to_string(),
|
||||
secret: "wrong secret".to_string(),
|
||||
mfa_token: None,
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Test recipient lookup
|
||||
assert_eq!(
|
||||
sql.recipient("john@example.org").await.unwrap(),
|
||||
Recipient::Account(Account {
|
||||
email: "john@example.org".to_string(),
|
||||
email_aliases: vec!["john.doe@example.org".to_string()],
|
||||
secret: Some("john secret".to_string()),
|
||||
groups: vec!["sales@example.org".to_string()],
|
||||
description: Some("John Doe".to_string()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
sql.recipient("jane@example.org").await.unwrap(),
|
||||
Recipient::Account(Account {
|
||||
email: "jane@example.org".to_string(),
|
||||
email_aliases: vec![],
|
||||
secret: Some("jane secret".to_string()),
|
||||
groups: vec!["sales@example.org".to_string()],
|
||||
description: Some("Jane Doe".to_string()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
sql.recipient("sales@example.org").await.unwrap(),
|
||||
Recipient::Group(Group {
|
||||
email: "sales@example.org".to_string(),
|
||||
email_aliases: vec![],
|
||||
description: Some("Sales Team".to_string())
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
sql.recipient("unknown@example.org").await.unwrap(),
|
||||
Recipient::Invalid
|
||||
);
|
||||
}
|
||||
|
||||
271
tests/src/directory/synchronization.rs
Normal file
271
tests/src/directory/synchronization.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::utils::server::TestServerBuilder;
|
||||
use registry::schema::{
|
||||
prelude::ObjectType,
|
||||
structs::{Account, Domain, EmailAlias},
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
pub async fn test() {
|
||||
let test = TestServerBuilder::new("directory_synchronization_test")
|
||||
.await
|
||||
.with_default_listeners()
|
||||
.await
|
||||
.disable_services()
|
||||
.build()
|
||||
.await;
|
||||
let admin = test.account("admin");
|
||||
|
||||
// Synchronizing an account with an unknown domain should fail
|
||||
assert!(
|
||||
test.server
|
||||
.synchronize_account(directory::Account {
|
||||
email: "john@unknown.org".to_string(),
|
||||
email_aliases: vec![],
|
||||
secret: "supersecret".to_string().into(),
|
||||
groups: vec![],
|
||||
description: "John Doe".to_string().into(),
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Initial account synchronization
|
||||
let mut account_in = directory::Account {
|
||||
email: "john@example.org".to_string(),
|
||||
email_aliases: vec![
|
||||
"john.doe@example.org".to_string(),
|
||||
"j.doe@example.org".to_string(),
|
||||
],
|
||||
secret: "supersecret".to_string().into(),
|
||||
groups: vec![
|
||||
"corporate@example.org".to_string(),
|
||||
"sales@example.org".to_string(),
|
||||
],
|
||||
description: "John Doe".to_string().into(),
|
||||
};
|
||||
let result = test
|
||||
.server
|
||||
.synchronize_account(account_in.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let account_id = Id::from(result.id);
|
||||
let account_out = test
|
||||
.server
|
||||
.registry()
|
||||
.object::<Account>(account_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_user()
|
||||
.unwrap();
|
||||
let domain_id = account_out.domain_id;
|
||||
assert_eq!(
|
||||
admin.registry_get::<Domain>(domain_id).await.name,
|
||||
"example.org"
|
||||
);
|
||||
assert_eq!(account_out.name, "john");
|
||||
assert_eq!(account_out.description.as_deref(), Some("John Doe"));
|
||||
assert_eq!(
|
||||
account_out
|
||||
.credentials
|
||||
.values()
|
||||
.next()
|
||||
.and_then(|v| v.as_main_credential())
|
||||
.map(|c| c.secret.as_str()),
|
||||
Some("supersecret")
|
||||
);
|
||||
assert_eq!(account_out.aliases.len(), 2);
|
||||
let aliases = account_out.aliases.iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
aliases[0],
|
||||
&EmailAlias {
|
||||
description: None,
|
||||
domain_id,
|
||||
enabled: true,
|
||||
name: "john.doe".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
aliases[1],
|
||||
&EmailAlias {
|
||||
description: None,
|
||||
domain_id,
|
||||
enabled: true,
|
||||
name: "j.doe".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(account_out.member_group_ids.len(), 2);
|
||||
for (idx, group_id) in account_out.member_group_ids.iter().enumerate() {
|
||||
let group = admin
|
||||
.registry_get::<Account>(*group_id)
|
||||
.await
|
||||
.into_group()
|
||||
.unwrap();
|
||||
assert_eq!(group.name, if idx == 0 { "corporate" } else { "sales" });
|
||||
assert_eq!(group.domain_id, domain_id);
|
||||
}
|
||||
assert_eq!(
|
||||
test.server
|
||||
.registry()
|
||||
.count_object(ObjectType::Account)
|
||||
.await
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
test.server
|
||||
.registry()
|
||||
.count_object(ObjectType::Domain)
|
||||
.await
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
|
||||
// No changes should not cause any updates
|
||||
assert_eq!(
|
||||
test.server
|
||||
.synchronize_account(account_in.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.id,
|
||||
account_id.document_id()
|
||||
);
|
||||
assert_eq!(
|
||||
test.server
|
||||
.registry()
|
||||
.object::<Account>(account_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_user()
|
||||
.unwrap(),
|
||||
account_out
|
||||
);
|
||||
assert_eq!(
|
||||
test.server
|
||||
.registry()
|
||||
.count_object(ObjectType::Account)
|
||||
.await
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
|
||||
// Make some changes and synchronize again
|
||||
account_in.description = "Johnathan Doe".to_string().into();
|
||||
account_in
|
||||
.email_aliases
|
||||
.push("johnny@example.org".to_string());
|
||||
account_in.groups.pop();
|
||||
account_in.groups.push("support@example.org".to_string());
|
||||
account_in.secret = "evenmoresecret".to_string().into();
|
||||
assert_eq!(
|
||||
test.server
|
||||
.synchronize_account(account_in.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.id,
|
||||
account_id.document_id()
|
||||
);
|
||||
let account_out = test
|
||||
.server
|
||||
.registry()
|
||||
.object::<Account>(account_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_user()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
account_out
|
||||
.credentials
|
||||
.values()
|
||||
.next()
|
||||
.and_then(|v| v.as_main_credential())
|
||||
.map(|c| c.secret.as_str()),
|
||||
Some("evenmoresecret")
|
||||
);
|
||||
assert_eq!(account_out.description.as_deref(), Some("Johnathan Doe"));
|
||||
assert_eq!(account_out.aliases.len(), 3);
|
||||
let aliases = account_out.aliases.iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
aliases[2],
|
||||
&EmailAlias {
|
||||
description: None,
|
||||
domain_id,
|
||||
enabled: true,
|
||||
name: "johnny".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(account_out.member_group_ids.len(), 2);
|
||||
let account_groups = account_out
|
||||
.member_group_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
for (idx, group_id) in account_groups.iter().enumerate() {
|
||||
let group = admin
|
||||
.registry_get::<Account>(*group_id)
|
||||
.await
|
||||
.into_group()
|
||||
.unwrap();
|
||||
assert_eq!(group.name, if idx == 0 { "corporate" } else { "support" });
|
||||
assert_eq!(group.domain_id, domain_id);
|
||||
}
|
||||
assert_eq!(
|
||||
test.server
|
||||
.registry()
|
||||
.count_object(ObjectType::Account)
|
||||
.await
|
||||
.unwrap(),
|
||||
4
|
||||
);
|
||||
|
||||
// Synchronize a group
|
||||
assert_eq!(
|
||||
test.server
|
||||
.synchronize_group(directory::Group {
|
||||
email: "corporate@example.org".to_string(),
|
||||
email_aliases: vec!["everyone@example.org".to_string()],
|
||||
description: "Corporate Group".to_string().into(),
|
||||
})
|
||||
.await
|
||||
.unwrap(),
|
||||
account_groups[0].document_id()
|
||||
);
|
||||
let group_out = test
|
||||
.server
|
||||
.registry()
|
||||
.object::<Account>(account_groups[0])
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_group()
|
||||
.unwrap();
|
||||
assert_eq!(group_out.name, "corporate");
|
||||
assert_eq!(group_out.description.as_deref(), Some("Corporate Group"));
|
||||
assert_eq!(group_out.aliases.len(), 1);
|
||||
let aliases = group_out.aliases.iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
aliases[0],
|
||||
&EmailAlias {
|
||||
description: None,
|
||||
domain_id,
|
||||
enabled: true,
|
||||
name: "everyone".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
test.server
|
||||
.registry()
|
||||
.count_object(ObjectType::Account)
|
||||
.await
|
||||
.unwrap(),
|
||||
4
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user