Registry testing - part 2
This commit is contained in:
@@ -5,8 +5,8 @@ edition = "2024"
|
||||
|
||||
[features]
|
||||
#default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "nats", "azure", "foundationdb"]
|
||||
#default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "foundationdb"]
|
||||
default = ["postgres"]
|
||||
default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "foundationdb"]
|
||||
#default = ["postgres"]
|
||||
sqlite = ["store/sqlite", "directory/sqlite"]
|
||||
foundationdb = ["store/foundation", "common/foundation"]
|
||||
postgres = ["store/postgres", "directory/postgres"]
|
||||
|
||||
@@ -29,11 +29,11 @@ pub mod jmap;
|
||||
#[cfg(test)]
|
||||
pub mod smtp;
|
||||
#[cfg(test)]
|
||||
pub mod store;
|
||||
#[cfg(test)]
|
||||
pub mod webdav;
|
||||
*/
|
||||
#[cfg(test)]
|
||||
pub mod store;
|
||||
#[cfg(test)]
|
||||
pub mod system;
|
||||
#[cfg(test)]
|
||||
pub mod utils;
|
||||
|
||||
@@ -4,457 +4,447 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::store::{CONFIG, TempDir, cleanup::store_destroy};
|
||||
use crate::utils::{cleanup::store_destroy, server::TestServerBuilder};
|
||||
use ahash::AHashMap;
|
||||
use common::{Core, Inner, Server, config::storage::Storage};
|
||||
use email::message::metadata::MessageMetadata;
|
||||
use std::sync::Arc;
|
||||
use registry::{
|
||||
schema::{enums::CompressionAlgo, structs::Jmap},
|
||||
types::duration::Duration,
|
||||
};
|
||||
use services::task_manager::destroy_account::destroy_account_blobs;
|
||||
use store::{
|
||||
BlobStore, Serialize, SerializeInfallible,
|
||||
write::{Archiver, BatchBuilder, BlobLink, BlobOp, ValueClass, blob::BlobQuota, now},
|
||||
write::{Archiver, BatchBuilder, BlobLink, BlobOp, ValueClass, now},
|
||||
};
|
||||
use types::{blob::BlobClass, blob_hash::BlobHash, collection::Collection, field::EmailField};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn blob_tests() {
|
||||
let temp_dir = TempDir::new("blob_tests", true);
|
||||
let mut config =
|
||||
Config::new(CONFIG.replace("{TMP}", temp_dir.path.as_path().to_str().unwrap())).unwrap();
|
||||
let stores = Stores::parse_all(&mut config, false).await;
|
||||
let test = TestServerBuilder::new("blob_tests", true)
|
||||
.await
|
||||
.with_object(Jmap {
|
||||
upload_quota: 1024,
|
||||
upload_ttl: Duration::from_millis(1000),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
for (store_id, blob_store) in &stores.blob_stores {
|
||||
println!("Testing blob store {}...", store_id);
|
||||
test_store(blob_store.clone()).await;
|
||||
let store = test.server.core.storage.data.clone();
|
||||
let blob_store = test.server.core.storage.blob.clone();
|
||||
|
||||
println!(
|
||||
"Testing blob store {} with data store {}...",
|
||||
std::env::var("BLOB_STORE").unwrap_or_else(|_| "default".to_string()),
|
||||
std::env::var("STORE").unwrap()
|
||||
);
|
||||
|
||||
// Test blob quota
|
||||
assert!(test.server.blob_has_quota(0, 1024).await.unwrap());
|
||||
assert!(!test.server.blob_has_quota(0, 1024).await.unwrap());
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
assert!(test.server.blob_has_quota(0, 1024).await.unwrap());
|
||||
|
||||
// Test and reset store
|
||||
test_store(blob_store.clone()).await;
|
||||
store_destroy(&store).await;
|
||||
|
||||
// Blob hash exists
|
||||
let hash = BlobHash::generate(b"abc".as_slice());
|
||||
assert!(!store.blob_exists(&hash).await.unwrap());
|
||||
|
||||
// Reserve blob
|
||||
let until = now() + 1;
|
||||
store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.with_account_id(0)
|
||||
.set(
|
||||
BlobOp::Link {
|
||||
to: BlobLink::Temporary { until },
|
||||
hash: hash.clone(),
|
||||
},
|
||||
1024u32.serialize(),
|
||||
)
|
||||
.build_all(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Uncommitted blob, should not exist
|
||||
assert!(!store.blob_exists(&hash).await.unwrap());
|
||||
|
||||
// Write blob to store
|
||||
blob_store
|
||||
.put_blob(hash.as_ref(), b"abc", CompressionAlgo::Lz4)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Commit blob
|
||||
store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.set(BlobOp::Commit { hash: hash.clone() }, Vec::new())
|
||||
.build_all(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob hash should now exist
|
||||
assert!(store.blob_exists(&hash).await.unwrap());
|
||||
assert!(
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// AccountId 0 should be able to read blob
|
||||
assert!(
|
||||
store
|
||||
.blob_has_access(
|
||||
&hash,
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: until
|
||||
}
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// AccountId 1 should not be able to read blob
|
||||
assert!(
|
||||
!store
|
||||
.blob_has_access(
|
||||
&hash,
|
||||
BlobClass::Reserved {
|
||||
account_id: 1,
|
||||
expires: until
|
||||
}
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// Purge expired blobs
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
store
|
||||
.purge_blobs_all_shards(blob_store.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob hash should no longer exist
|
||||
assert!(!store.blob_exists(&hash).await.unwrap());
|
||||
|
||||
// AccountId 0 should not be able to read blob
|
||||
assert!(
|
||||
!store
|
||||
.blob_has_access(
|
||||
&hash,
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: until
|
||||
}
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// Blob should no longer be in store
|
||||
assert!(
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// Upload one linked blob to accountId 1, two linked blobs to accountId 0, and three unlinked (reserved) blobs to accountId 2
|
||||
let expiry_times = AHashMap::from_iter([
|
||||
(b"abc", now() - 10),
|
||||
(b"efg", now() + 10),
|
||||
(b"hij", now() + 10),
|
||||
]);
|
||||
for (document_id, (blob, _)) in [
|
||||
(b"123", vec![]),
|
||||
(b"456", vec![]),
|
||||
(b"789", vec![]),
|
||||
(b"abc", 5000u32.serialize()),
|
||||
(b"efg", 1000u32.serialize()),
|
||||
(b"hij", 2000u32.serialize()),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(if document_id > 0 { 0 } else { 1 })
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(document_id as u32);
|
||||
if let Some(until) = expiry_times.get(blob) {
|
||||
batch.set(
|
||||
BlobOp::Link {
|
||||
hash: hash.clone(),
|
||||
to: BlobLink::Temporary { until: *until },
|
||||
},
|
||||
vec![],
|
||||
);
|
||||
} else {
|
||||
batch
|
||||
.set(
|
||||
BlobOp::Link {
|
||||
hash: hash.clone(),
|
||||
to: BlobLink::Document,
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.set(
|
||||
ValueClass::Property(EmailField::Metadata.into()),
|
||||
Archiver::new(MessageMetadata {
|
||||
contents: Default::default(),
|
||||
rcvd_attach: Default::default(),
|
||||
blob_hash: hash.clone(),
|
||||
blob_body_offset: Default::default(),
|
||||
preview: Default::default(),
|
||||
raw_headers: Default::default(),
|
||||
})
|
||||
.serialize()
|
||||
.unwrap(),
|
||||
);
|
||||
};
|
||||
batch.set(BlobOp::Commit { hash: hash.clone() }, vec![]);
|
||||
|
||||
store.write(batch.build_all()).await.unwrap();
|
||||
blob_store
|
||||
.put_blob(hash.as_ref(), blob.as_slice(), CompressionAlgo::Lz4)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for (store_id, store) in stores.stores {
|
||||
println!("Testing blob management on store {}...", store_id);
|
||||
|
||||
// Init store
|
||||
store_destroy(&store).await;
|
||||
|
||||
// Test internal blob store
|
||||
let blob_store: BlobStore = store.clone().into();
|
||||
let server = Server {
|
||||
inner: Arc::new(Inner::default()),
|
||||
core: Arc::new(Core {
|
||||
storage: Storage {
|
||||
data: store.clone(),
|
||||
blob: blob_store.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
// Blob hash exists
|
||||
let hash = BlobHash::generate(b"abc".as_slice());
|
||||
assert!(!store.blob_exists(&hash).await.unwrap());
|
||||
|
||||
// Reserve blob
|
||||
let until = now() + 1;
|
||||
store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.with_account_id(0)
|
||||
.set(
|
||||
BlobOp::Link {
|
||||
to: BlobLink::Temporary { until },
|
||||
hash: hash.clone(),
|
||||
},
|
||||
1024u32.serialize(),
|
||||
)
|
||||
.build_all(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Uncommitted blob, should not exist
|
||||
assert!(!store.blob_exists(&hash).await.unwrap());
|
||||
|
||||
// Write blob to store
|
||||
blob_store.put_blob(hash.as_ref(), b"abc").await.unwrap();
|
||||
|
||||
// Commit blob
|
||||
store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.set(BlobOp::Commit { hash: hash.clone() }, Vec::new())
|
||||
.build_all(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob hash should now exist
|
||||
assert!(store.blob_exists(&hash).await.unwrap());
|
||||
// Purge expired blobs and make sure nothing else is deleted
|
||||
store
|
||||
.purge_blobs_all_shards(blob_store.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
for (pos, (blob, blob_class)) in [
|
||||
(
|
||||
b"abc",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"abc"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"123",
|
||||
BlobClass::Linked {
|
||||
account_id: 1,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"456",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 1,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"789",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 2,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"efg",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"efg"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"hij",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"hij"],
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
let ct = pos == 0;
|
||||
assert!(store.blob_has_access(&hash, blob_class).await.unwrap() ^ ct);
|
||||
assert!(store.blob_exists(&hash).await.unwrap() ^ ct);
|
||||
assert!(
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
^ ct
|
||||
);
|
||||
}
|
||||
|
||||
// AccountId 0 should be able to read blob
|
||||
assert!(
|
||||
store
|
||||
.blob_has_access(
|
||||
&hash,
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: until
|
||||
}
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
// AccountId 0 should not have access to accountId 1's blobs
|
||||
assert!(
|
||||
!store
|
||||
.blob_has_access(
|
||||
BlobHash::generate(b"123".as_slice()),
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
}
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// AccountId 1 should not be able to read blob
|
||||
assert!(
|
||||
!store
|
||||
.blob_has_access(
|
||||
&hash,
|
||||
BlobClass::Reserved {
|
||||
account_id: 1,
|
||||
expires: until
|
||||
}
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
// Unlink blob
|
||||
store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.with_account_id(0)
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(2)
|
||||
.clear(BlobOp::Link {
|
||||
hash: BlobHash::generate(b"789".as_slice()),
|
||||
to: BlobLink::Document,
|
||||
})
|
||||
.build_all(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob already expired, quota should be 0
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
assert_eq!(
|
||||
store.blob_quota(0).await.unwrap(),
|
||||
BlobQuota { bytes: 0, count: 0 }
|
||||
);
|
||||
|
||||
// Purge expired blobs
|
||||
store.purge_blobs(blob_store.clone()).await.unwrap();
|
||||
|
||||
// Blob hash should no longer exist
|
||||
assert!(!store.blob_exists(&hash).await.unwrap());
|
||||
|
||||
// AccountId 0 should not be able to read blob
|
||||
assert!(
|
||||
!store
|
||||
.blob_has_access(
|
||||
&hash,
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: until
|
||||
}
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// Blob should no longer be in store
|
||||
// Purge and make sure blob is deleted
|
||||
store
|
||||
.purge_blobs_all_shards(blob_store.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
for (pos, (blob, blob_class)) in [
|
||||
(
|
||||
b"789",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 2,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"123",
|
||||
BlobClass::Linked {
|
||||
account_id: 1,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"456",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 1,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"efg",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"efg"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"hij",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"hij"],
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let ct = pos == 0;
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
assert!(store.blob_has_access(&hash, blob_class).await.unwrap() ^ ct);
|
||||
assert!(store.blob_exists(&hash).await.unwrap() ^ ct);
|
||||
assert!(
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
.is_some()
|
||||
^ ct
|
||||
);
|
||||
}
|
||||
|
||||
// Upload one linked blob to accountId 1, two linked blobs to accountId 0, and three unlinked (reserved) blobs to accountId 2
|
||||
let expiry_times = AHashMap::from_iter([
|
||||
(b"abc", now() - 10),
|
||||
(b"efg", now() + 10),
|
||||
(b"hij", now() + 10),
|
||||
]);
|
||||
for (document_id, (blob, blob_value)) in [
|
||||
(b"123", vec![]),
|
||||
(b"456", vec![]),
|
||||
(b"789", vec![]),
|
||||
(b"abc", 5000u32.serialize()),
|
||||
(b"efg", 1000u32.serialize()),
|
||||
(b"hij", 2000u32.serialize()),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(if document_id > 0 { 0 } else { 1 })
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(document_id as u32);
|
||||
if let Some(until) = expiry_times.get(blob) {
|
||||
if !blob_value.is_empty() {
|
||||
batch.set(
|
||||
BlobOp::Quota {
|
||||
hash: hash.clone(),
|
||||
until: *until,
|
||||
},
|
||||
blob_value,
|
||||
);
|
||||
}
|
||||
batch.set(
|
||||
BlobOp::Link {
|
||||
hash: hash.clone(),
|
||||
to: BlobLink::Temporary { until: *until },
|
||||
},
|
||||
vec![],
|
||||
);
|
||||
} else {
|
||||
batch
|
||||
.set(
|
||||
BlobOp::Link {
|
||||
hash: hash.clone(),
|
||||
to: BlobLink::Document,
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.set(
|
||||
ValueClass::Property(EmailField::Metadata.into()),
|
||||
Archiver::new(MessageMetadata {
|
||||
contents: Default::default(),
|
||||
rcvd_attach: Default::default(),
|
||||
blob_hash: hash.clone(),
|
||||
blob_body_offset: Default::default(),
|
||||
preview: Default::default(),
|
||||
raw_headers: Default::default(),
|
||||
})
|
||||
.serialize()
|
||||
.unwrap(),
|
||||
);
|
||||
};
|
||||
batch.set(BlobOp::Commit { hash: hash.clone() }, vec![]);
|
||||
// Unlink all blobs from accountId 1 and purge
|
||||
destroy_account_blobs(&test.server, 1).await.unwrap();
|
||||
store
|
||||
.purge_blobs_all_shards(blob_store.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
store.write(batch.build_all()).await.unwrap();
|
||||
blob_store
|
||||
.put_blob(hash.as_ref(), blob.as_slice())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// One of the reserved blobs expired and should not count towards quota
|
||||
assert_eq!(
|
||||
store.blob_quota(0).await.unwrap(),
|
||||
BlobQuota {
|
||||
bytes: 3000,
|
||||
count: 2
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.blob_quota(1).await.unwrap(),
|
||||
BlobQuota { bytes: 0, count: 0 }
|
||||
);
|
||||
|
||||
// Purge expired blobs and make sure nothing else is deleted
|
||||
store.purge_blobs(blob_store.clone()).await.unwrap();
|
||||
for (pos, (blob, blob_class)) in [
|
||||
(
|
||||
b"abc",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"abc"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"123",
|
||||
BlobClass::Linked {
|
||||
account_id: 1,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"456",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 1,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"789",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 2,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"efg",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"efg"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"hij",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"hij"],
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let ct = pos == 0;
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
assert!(store.blob_has_access(&hash, blob_class).await.unwrap() ^ ct);
|
||||
assert!(store.blob_exists(&hash).await.unwrap() ^ ct);
|
||||
assert!(
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
^ ct
|
||||
);
|
||||
}
|
||||
|
||||
// AccountId 0 should not have access to accountId 1's blobs
|
||||
// Make sure only accountId 0's blobs are left
|
||||
for (pos, (blob, blob_class)) in [
|
||||
(
|
||||
b"123",
|
||||
BlobClass::Linked {
|
||||
account_id: 1,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"456",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 1,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"efg",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"efg"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"hij",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"hij"],
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let ct = pos == 0;
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
assert!(store.blob_has_access(&hash, blob_class).await.unwrap() ^ ct);
|
||||
assert!(store.blob_exists(&hash).await.unwrap() ^ ct);
|
||||
assert!(
|
||||
!store
|
||||
.blob_has_access(
|
||||
BlobHash::generate(b"123".as_slice()),
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
}
|
||||
)
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
^ ct
|
||||
);
|
||||
|
||||
// Unlink blob
|
||||
store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.with_account_id(0)
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(2)
|
||||
.clear(BlobOp::Link {
|
||||
hash: BlobHash::generate(b"789".as_slice()),
|
||||
to: BlobLink::Document,
|
||||
})
|
||||
.build_all(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Purge and make sure blob is deleted
|
||||
store.purge_blobs(blob_store.clone()).await.unwrap();
|
||||
for (pos, (blob, blob_class)) in [
|
||||
(
|
||||
b"789",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 2,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"123",
|
||||
BlobClass::Linked {
|
||||
account_id: 1,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"456",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 1,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"efg",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"efg"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"hij",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"hij"],
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let ct = pos == 0;
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
assert!(store.blob_has_access(&hash, blob_class).await.unwrap() ^ ct);
|
||||
assert!(store.blob_exists(&hash).await.unwrap() ^ ct);
|
||||
assert!(
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
^ ct
|
||||
);
|
||||
}
|
||||
|
||||
// Unlink all blobs from accountId 1 and purge
|
||||
destroy_account_blobs(&server, 1).await.unwrap();
|
||||
store.purge_blobs(blob_store.clone()).await.unwrap();
|
||||
|
||||
// Make sure only accountId 0's blobs are left
|
||||
for (pos, (blob, blob_class)) in [
|
||||
(
|
||||
b"123",
|
||||
BlobClass::Linked {
|
||||
account_id: 1,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"456",
|
||||
BlobClass::Linked {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 1,
|
||||
},
|
||||
),
|
||||
(
|
||||
b"efg",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"efg"],
|
||||
},
|
||||
),
|
||||
(
|
||||
b"hij",
|
||||
BlobClass::Reserved {
|
||||
account_id: 0,
|
||||
expires: expiry_times[&b"hij"],
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let ct = pos == 0;
|
||||
let hash = BlobHash::generate(blob.as_slice());
|
||||
assert!(store.blob_has_access(&hash, blob_class).await.unwrap() ^ ct);
|
||||
assert!(store.blob_exists(&hash).await.unwrap() ^ ct);
|
||||
assert!(
|
||||
blob_store
|
||||
.get_blob(hash.as_ref(), 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
^ ct
|
||||
);
|
||||
}
|
||||
}
|
||||
temp_dir.delete();
|
||||
|
||||
test.temp_dir.delete();
|
||||
}
|
||||
|
||||
async fn test_store(store: BlobStore) {
|
||||
@@ -462,7 +452,10 @@ async fn test_store(store: BlobStore) {
|
||||
const DATA: &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce erat nisl, dignissim a porttitor id, varius nec arcu. Sed mauris.";
|
||||
let hash = BlobHash::generate(DATA);
|
||||
|
||||
store.put_blob(hash.as_slice(), DATA).await.unwrap();
|
||||
store
|
||||
.put_blob(hash.as_slice(), DATA, CompressionAlgo::Lz4)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
store
|
||||
@@ -502,7 +495,10 @@ async fn test_store(store: BlobStore) {
|
||||
data.extend_from_slice(marker.as_bytes());
|
||||
}
|
||||
let hash = BlobHash::generate(&data);
|
||||
store.put_blob(hash.as_slice(), &data).await.unwrap();
|
||||
store
|
||||
.put_blob(hash.as_slice(), &data, CompressionAlgo::Lz4)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
store
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::store::{
|
||||
TempDir,
|
||||
cleanup::{store_assert_is_empty, store_destroy},
|
||||
use crate::{
|
||||
store::TempDir,
|
||||
utils::{
|
||||
cleanup::{store_assert_is_empty, store_destroy},
|
||||
server::TestServer,
|
||||
},
|
||||
};
|
||||
use ::registry::schema::enums::CompressionAlgo;
|
||||
use ahash::AHashSet;
|
||||
use common::{Core, DATABASE_SCHEMA_VERSION, manager::backup::BackupParams};
|
||||
use common::{DATABASE_SCHEMA_VERSION, manager::backup::BackupParams};
|
||||
use store::{
|
||||
rand,
|
||||
write::{
|
||||
AnyClass, AnyKey, BatchBuilder, BlobLink, BlobOp, Operation, QueueClass, QueueEvent,
|
||||
ValueClass,
|
||||
RegistryClass, ValueClass,
|
||||
},
|
||||
*,
|
||||
};
|
||||
@@ -24,15 +28,10 @@ use types::{
|
||||
field::{Field, MailboxField},
|
||||
};
|
||||
|
||||
pub async fn test(db: Store) {
|
||||
let mut core = Core::default();
|
||||
core.storage.data = db.clone();
|
||||
core.storage.blob = db.clone().into();
|
||||
core.storage.fts = db.clone().into();
|
||||
core.storage.lookup = db.clone().into();
|
||||
|
||||
pub async fn test(test: &TestServer) {
|
||||
// Make sure the store is empty
|
||||
store_assert_is_empty(&db, db.clone().into(), true).await;
|
||||
store_assert_is_empty(test.server.store(), test.server.blob_store().clone(), true).await;
|
||||
let db = test.server.store().clone();
|
||||
|
||||
// Create blobs
|
||||
println!("Creating blobs...");
|
||||
@@ -49,9 +48,9 @@ pub async fn test(db: Store) {
|
||||
let data = random_bytes(blob_size);
|
||||
let hash = BlobHash::generate(data.as_slice());
|
||||
blob_hashes.push(hash.clone());
|
||||
core.storage
|
||||
.blob
|
||||
.put_blob(hash.as_ref(), &data)
|
||||
test.server
|
||||
.blob_store()
|
||||
.put_blob(hash.as_ref(), &data, CompressionAlgo::Lz4)
|
||||
.await
|
||||
.unwrap();
|
||||
batch.set(ValueClass::Blob(BlobOp::Commit { hash }), vec![]);
|
||||
@@ -142,16 +141,11 @@ pub async fn test(db: Store) {
|
||||
})),
|
||||
random_bytes(idx),
|
||||
);
|
||||
/*batch.set(
|
||||
ValueClass::InMemory(InMemoryClass::Key(random_bytes(idx))),
|
||||
random_bytes(idx),
|
||||
);
|
||||
batch.add(
|
||||
ValueClass::InMemory(InMemoryClass::Counter(random_bytes(idx))),
|
||||
rand::random(),
|
||||
);*/
|
||||
batch.set(
|
||||
ValueClass::Config(random_bytes(idx + 10)),
|
||||
ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: 0,
|
||||
item_id: 1,
|
||||
}),
|
||||
random_bytes(idx + 10),
|
||||
);
|
||||
}
|
||||
@@ -167,40 +161,7 @@ pub async fn test(db: Store) {
|
||||
for account_id in [1, 2, 3, 4, 5] {
|
||||
batch
|
||||
.with_document(account_id)
|
||||
.add(
|
||||
ValueClass::Directory(DirectoryClass::UsedQuota(account_id)),
|
||||
rand::random(),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryClass::NameToId(random_bytes(
|
||||
2 + account_id as usize,
|
||||
))),
|
||||
random_bytes(4),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryClass::EmailToId(random_bytes(
|
||||
4 + account_id as usize,
|
||||
))),
|
||||
random_bytes(4),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryClass::Principal(account_id)),
|
||||
random_bytes(30),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryClass::MemberOf {
|
||||
principal_id: account_id,
|
||||
member_of: rand::random(),
|
||||
}),
|
||||
random_bytes(15),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryClass::Members {
|
||||
principal_id: account_id,
|
||||
has_member: rand::random(),
|
||||
}),
|
||||
random_bytes(15),
|
||||
);
|
||||
.add(ValueClass::Quota, account_id as i64 * 1000);
|
||||
}
|
||||
db.write(batch.build_all()).await.unwrap();
|
||||
|
||||
@@ -212,7 +173,10 @@ pub async fn test(db: Store) {
|
||||
// Export store
|
||||
println!("Exporting store...");
|
||||
let temp_dir = TempDir::new("art_vandelay_tests", true);
|
||||
core.backup(BackupParams::new(temp_dir.path.clone())).await;
|
||||
test.server
|
||||
.core
|
||||
.backup(BackupParams::new(temp_dir.path.clone()))
|
||||
.await;
|
||||
|
||||
// Destroy store
|
||||
println!("Destroying store...");
|
||||
@@ -221,7 +185,7 @@ pub async fn test(db: Store) {
|
||||
|
||||
// Import store
|
||||
println!("Importing store...");
|
||||
core.restore(temp_dir.path.clone()).await;
|
||||
test.server.core.restore(temp_dir.path.clone()).await;
|
||||
|
||||
// Verify hash
|
||||
print!("Verifying store hash...");
|
||||
@@ -266,6 +230,8 @@ impl Snapshot {
|
||||
(SUBSPACE_IN_MEMORY_VALUE, true),
|
||||
(SUBSPACE_PROPERTY, true),
|
||||
(SUBSPACE_REGISTRY, true),
|
||||
(SUBSPACE_REGISTRY_IDX, !is_sql),
|
||||
(SUBSPACE_REGISTRY_PK, true),
|
||||
(SUBSPACE_QUEUE_MESSAGE, true),
|
||||
(SUBSPACE_QUEUE_EVENT, true),
|
||||
(SUBSPACE_QUOTA, !is_sql),
|
||||
|
||||
@@ -4,285 +4,284 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
AssertConfig,
|
||||
store::{
|
||||
CONFIG, TempDir,
|
||||
cleanup::{store_assert_is_empty, store_destroy},
|
||||
},
|
||||
use crate::utils::{
|
||||
cleanup::{store_assert_is_empty, store_destroy},
|
||||
server::TestServerBuilder,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use registry::schema::structs::Rate;
|
||||
use registry::types::duration::Duration;
|
||||
use store::{InMemoryStore, dispatch::lookup::KeyValue};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn lookup_tests() {
|
||||
let temp_dir = TempDir::new("lookup_tests", true);
|
||||
let mut config =
|
||||
Config::new(CONFIG.replace("{TMP}", temp_dir.path.as_path().to_str().unwrap()))
|
||||
.unwrap()
|
||||
.assert_no_errors();
|
||||
let stores = Stores::parse_all(&mut config, false).await;
|
||||
let insert = std::env::var("NO_INSERT").is_err();
|
||||
let test = TestServerBuilder::new("lookup_tests", insert)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
let store = test.server.in_memory_store().clone();
|
||||
let rate = Rate {
|
||||
requests: 1,
|
||||
period: Duration::from_secs(1),
|
||||
count: 1,
|
||||
period: Duration::from_millis(1000),
|
||||
};
|
||||
|
||||
for (store_id, store) in stores.in_memory_stores {
|
||||
println!("Testing in-memory store {}...", store_id);
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_destroy(store).await;
|
||||
} else {
|
||||
// Reset redis counter
|
||||
store
|
||||
.key_set(KeyValue::new("abc", "0".as_bytes().to_vec()))
|
||||
.await
|
||||
.unwrap();
|
||||
println!(
|
||||
"Testing in-memory store {}...",
|
||||
std::env::var("MEMORY_STORE").unwrap_or_else(|_| "default".to_string())
|
||||
);
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_destroy(store).await;
|
||||
} else {
|
||||
// Reset redis counter
|
||||
store
|
||||
.key_set(KeyValue::new("abc", "0".as_bytes().to_vec()))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test key
|
||||
let key = "xyz".as_bytes().to_vec();
|
||||
store
|
||||
.key_set(KeyValue::new(key.clone(), "world".to_string().into_bytes()))
|
||||
.await
|
||||
.unwrap();
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
assert_eq!(
|
||||
store.key_get::<String>(key.clone()).await.unwrap(),
|
||||
Some("world".to_string())
|
||||
);
|
||||
|
||||
// Test value expiry
|
||||
store
|
||||
.key_set(KeyValue::new(key.clone(), "hello".to_string().into_bytes()).expires(1))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store.key_get::<String>(key.clone()).await.unwrap(),
|
||||
Some("hello".to_string())
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
assert_eq!(None, store.key_get::<String>(key.clone()).await.unwrap());
|
||||
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
|
||||
// Test counter
|
||||
let key = "abc".as_bytes().to_vec();
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), 1), true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(1, store.counter_get(key.clone()).await.unwrap());
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), 2), true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(3, store.counter_get(key.clone()).await.unwrap());
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), -3), false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(0, store.counter_get(key.clone()).await.unwrap());
|
||||
|
||||
// Test counter expiry
|
||||
let key = "fgh".as_bytes().to_vec();
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), 1).expires(1), false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(1, store.counter_get(key.clone()).await.unwrap());
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
assert_eq!(0, store.counter_get(key.clone()).await.unwrap());
|
||||
|
||||
// Test rate limiter
|
||||
assert!(
|
||||
store
|
||||
.is_rate_allowed(0, "rate".as_bytes(), &rate, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.is_rate_allowed(0, "rate".as_bytes(), &rate, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
assert!(
|
||||
store
|
||||
.is_rate_allowed(0, "rate".as_bytes(), &rate, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
|
||||
// Test locking
|
||||
for iteration in [1, 2] {
|
||||
let mut tasks = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let store = store.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
store.try_lock(0, "lock".as_bytes(), 1).await.unwrap()
|
||||
}));
|
||||
}
|
||||
// Only one should return true
|
||||
let mut count = 0;
|
||||
for task in tasks {
|
||||
if task.await.unwrap() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(1, count, "Iteration {}", iteration);
|
||||
|
||||
// Test key
|
||||
let key = "xyz".as_bytes().to_vec();
|
||||
store
|
||||
.key_set(KeyValue::new(key.clone(), "world".to_string().into_bytes()))
|
||||
.await
|
||||
.unwrap();
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
assert_eq!(
|
||||
store.key_get::<String>(key.clone()).await.unwrap(),
|
||||
Some("world".to_string())
|
||||
);
|
||||
|
||||
// Test value expiry
|
||||
store
|
||||
.key_set(KeyValue::new(key.clone(), "hello".to_string().into_bytes()).expires(1))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store.key_get::<String>(key.clone()).await.unwrap(),
|
||||
Some("hello".to_string())
|
||||
);
|
||||
// Wait 2 seconds for the lock to expire
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
assert_eq!(None, store.key_get::<String>(key.clone()).await.unwrap());
|
||||
}
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
|
||||
// Test counter
|
||||
let key = "abc".as_bytes().to_vec();
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), 1), true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(1, store.counter_get(key.clone()).await.unwrap());
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), 2), true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(3, store.counter_get(key.clone()).await.unwrap());
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), -3), false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(0, store.counter_get(key.clone()).await.unwrap());
|
||||
|
||||
// Test counter expiry
|
||||
let key = "fgh".as_bytes().to_vec();
|
||||
store
|
||||
.counter_incr(KeyValue::new(key.clone(), 1).expires(1), false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(1, store.counter_get(key.clone()).await.unwrap());
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
assert_eq!(0, store.counter_get(key.clone()).await.unwrap());
|
||||
|
||||
// Test rate limiter
|
||||
assert!(
|
||||
store
|
||||
.is_rate_allowed(0, "rate".as_bytes(), &rate, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.is_rate_allowed(0, "rate".as_bytes(), &rate, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
assert!(
|
||||
store
|
||||
.is_rate_allowed(0, "rate".as_bytes(), &rate, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
|
||||
// Test locking
|
||||
for iteration in [1, 2] {
|
||||
let mut tasks = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let store = store.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
store.try_lock(0, "lock".as_bytes(), 1).await.unwrap()
|
||||
}));
|
||||
}
|
||||
// Only one should return true
|
||||
let mut count = 0;
|
||||
for task in tasks {
|
||||
if task.await.unwrap() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(1, count, "Iteration {}", iteration);
|
||||
|
||||
// Wait 2 seconds for the lock to expire
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
store.purge_in_memory_store().await.unwrap();
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
|
||||
// Test prefix delete
|
||||
// Test prefix delete
|
||||
store
|
||||
.key_set(KeyValue::with_prefix(
|
||||
1,
|
||||
[0],
|
||||
"hello".to_string().into_bytes(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
for v in 0u32..2020u32 {
|
||||
store
|
||||
.key_set(KeyValue::with_prefix(
|
||||
1,
|
||||
[0],
|
||||
"hello".to_string().into_bytes(),
|
||||
0,
|
||||
pack_u32(0, v),
|
||||
"world".to_string().into_bytes(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
for v in 0u32..2020u32 {
|
||||
store
|
||||
.key_set(KeyValue::with_prefix(
|
||||
0,
|
||||
pack_u32(0, v),
|
||||
"world".to_string().into_bytes(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.counter_incr(
|
||||
KeyValue::with_prefix(0, pack_u32(1, v), 123).expires(3600),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Make sure the keys are there
|
||||
assert_eq!(
|
||||
Some("hello"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
Some("world"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(0, pack_u32(0, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
}
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_ne!(
|
||||
0,
|
||||
store
|
||||
.counter_get(KeyValue::<()>::build_key(0, pack_u32(1, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
// Delete [0, 0, 0, 0, 1] prefix and make sure only the keys with that prefix are gone
|
||||
store
|
||||
.key_delete_prefix(&KeyValue::<()>::build_key(0, 1u32.to_be_bytes()))
|
||||
.counter_incr(
|
||||
KeyValue::with_prefix(0, pack_u32(1, v), 123).expires(3600),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Some("hello"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
Some("world"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(0, pack_u32(0, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
0,
|
||||
store
|
||||
.counter_get(KeyValue::<()>::build_key(0, pack_u32(1, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
// Delete [0, 0, 0, 0, 0] prefix and make sure only the keys with that prefix are gone
|
||||
// Make sure the keys are there
|
||||
assert_eq!(
|
||||
Some("hello"),
|
||||
store
|
||||
.key_delete_prefix(&KeyValue::<()>::build_key(0, 0u32.to_be_bytes()))
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
Some("hello"),
|
||||
Some("world"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.key_get::<String>(KeyValue::<()>::build_key(0, pack_u32(0, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
None,
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(0, pack_u32(0, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
}
|
||||
}
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_ne!(
|
||||
0,
|
||||
store
|
||||
.counter_get(KeyValue::<()>::build_key(0, pack_u32(1, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
// Delete [1, ...] prefix and make sure it's all gone
|
||||
store.key_delete_prefix(&[1u8]).await.unwrap();
|
||||
// Delete [0, 0, 0, 0, 1] prefix and make sure only the keys with that prefix are gone
|
||||
store
|
||||
.key_delete_prefix(&KeyValue::<()>::build_key(0, 1u32.to_be_bytes()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some("hello"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
Some("world"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(0, pack_u32(0, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
0,
|
||||
store
|
||||
.counter_get(KeyValue::<()>::build_key(0, pack_u32(1, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
// Delete [0, 0, 0, 0, 0] prefix and make sure only the keys with that prefix are gone
|
||||
store
|
||||
.key_delete_prefix(&KeyValue::<()>::build_key(0, 0u32.to_be_bytes()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some("hello"),
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
for v in [0, 1000, 1001, 2000, 2001] {
|
||||
assert_eq!(
|
||||
None,
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.key_get::<String>(KeyValue::<()>::build_key(0, pack_u32(0, v)))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
// Delete [1, ...] prefix and make sure it's all gone
|
||||
store.key_delete_prefix(&[1u8]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
None,
|
||||
store
|
||||
.key_get::<String>(KeyValue::<()>::build_key(1, [0]))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
);
|
||||
|
||||
if let InMemoryStore::Store(store) = &store {
|
||||
store_assert_is_empty(store, store.clone().into(), false).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,12 @@
|
||||
*/
|
||||
|
||||
pub mod blob;
|
||||
pub mod cleanup;
|
||||
pub mod import_export;
|
||||
pub mod lookup;
|
||||
pub mod ops;
|
||||
pub mod query;
|
||||
|
||||
use crate::{
|
||||
AssertConfig,
|
||||
store::cleanup::{search_store_destroy, store_destroy},
|
||||
};
|
||||
use crate::utils::server::TestServerBuilder;
|
||||
use std::io::Read;
|
||||
|
||||
pub struct TempDir {
|
||||
@@ -23,60 +19,41 @@ pub struct TempDir {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
pub async fn store_tests() {
|
||||
let insert = true;
|
||||
let temp_dir = TempDir::new("store_tests", insert);
|
||||
let mut config = Config::new(build_store_config(&temp_dir.path.to_string_lossy()))
|
||||
.unwrap()
|
||||
.assert_no_errors();
|
||||
let stores = Stores::parse_all(&mut config, false).await;
|
||||
let insert = std::env::var("NO_INSERT").is_err();
|
||||
let test = TestServerBuilder::new("store_tests", insert)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let store_id = std::env::var("STORE")
|
||||
.expect("Missing store type. Try running `STORE=<store_type> cargo test`");
|
||||
let store = stores
|
||||
.stores
|
||||
.get(&store_id)
|
||||
.expect("Store not found")
|
||||
.clone();
|
||||
println!("Testing store {}...", std::env::var("STORE").unwrap());
|
||||
|
||||
println!("Testing store {}...", store_id);
|
||||
if insert {
|
||||
store_destroy(&store).await;
|
||||
}
|
||||
test.destroy_store().await;
|
||||
|
||||
import_export::test(store.clone()).await;
|
||||
ops::test(store.clone()).await;
|
||||
import_export::test(&test).await;
|
||||
ops::test(&test).await;
|
||||
|
||||
if insert {
|
||||
temp_dir.delete();
|
||||
test.temp_dir.delete();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
pub async fn search_tests() {
|
||||
let insert = std::env::var("NO_INSERT").is_err();
|
||||
let temp_dir = TempDir::new("search_store_tests", insert);
|
||||
let mut config = Config::new(build_store_config(&temp_dir.path.to_string_lossy()))
|
||||
.unwrap()
|
||||
.assert_no_errors();
|
||||
let stores = Stores::parse_all(&mut config, false).await;
|
||||
let test = TestServerBuilder::new("search_store_tests", insert)
|
||||
.await
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let store_id = std::env::var("SEARCH_STORE")
|
||||
.expect("Missing store type. Try running `SEARCH_STORE=<store_type> cargo test`");
|
||||
let store = stores
|
||||
.search_stores
|
||||
.get(&store_id)
|
||||
.expect("Store not found")
|
||||
.clone();
|
||||
println!(
|
||||
"Testing search store {}...",
|
||||
std::env::var("SEARCH_STORE").unwrap_or("default".to_string())
|
||||
);
|
||||
|
||||
println!("Testing store {}...", store_id);
|
||||
if insert {
|
||||
search_store_destroy(&store).await;
|
||||
}
|
||||
|
||||
query::test(store, insert).await;
|
||||
query::test(&test, insert).await;
|
||||
|
||||
if insert {
|
||||
temp_dir.delete();
|
||||
test.temp_dir.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,123 +85,3 @@ impl TempDir {
|
||||
std::fs::remove_dir_all(&self.path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_store_config(temp_dir: &str) -> String {
|
||||
let store = std::env::var("STORE")
|
||||
.expect("Missing store type. Try running `STORE=<store_type> cargo test`");
|
||||
let fts_store = std::env::var("SEARCH_STORE").unwrap_or_else(|_| store.clone());
|
||||
let blob_store = std::env::var("BLOB_STORE").unwrap_or_else(|_| store.clone());
|
||||
let lookup_store = std::env::var("LOOKUP_STORE").unwrap_or_else(|_| store.clone());
|
||||
|
||||
CONFIG
|
||||
.replace("{STORE}", &store)
|
||||
.replace("{SEARCH_STORE}", &fts_store)
|
||||
.replace("{BLOB_STORE}", &blob_store)
|
||||
.replace("{LOOKUP_STORE}", &lookup_store)
|
||||
.replace("{TMP}", temp_dir)
|
||||
.replace(
|
||||
"{ELASTIC_ENABLED}",
|
||||
if fts_store != "elastic" {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
"{MEILI_ENABLED}",
|
||||
if fts_store != "meili" {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const CONFIG: &str = r#"
|
||||
[store."sqlite"]
|
||||
type = "sqlite"
|
||||
path = "{TMP}/sqlite.db"
|
||||
|
||||
[store."rocksdb"]
|
||||
type = "rocksdb"
|
||||
path = "{TMP}/rocks.db"
|
||||
|
||||
[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"
|
||||
|
||||
[store."elastic"]
|
||||
type = "elasticsearch"
|
||||
url = "https://localhost:9200"
|
||||
tls.allow-invalid-certs = true
|
||||
disable = {ELASTIC_ENABLED}
|
||||
[store."elastic".auth]
|
||||
username = "elastic"
|
||||
secret = "changeme"
|
||||
|
||||
[store."meili"]
|
||||
type = "meilisearch"
|
||||
url = "http://localhost:7700"
|
||||
tls.allow-invalid-certs = true
|
||||
disable = {MEILI_ENABLED}
|
||||
[store."meili".task]
|
||||
poll-interval = "100ms"
|
||||
#[store."meili".auth]
|
||||
#username = "meili"
|
||||
#secret = "changeme"
|
||||
|
||||
#[store."s3"]
|
||||
#type = "s3"
|
||||
#access-key = "minioadmin"
|
||||
#secret-key = "minioadmin"
|
||||
#region = "eu-central-1"
|
||||
#endpoint = "http://localhost:9000"
|
||||
#bucket = "tmp"
|
||||
|
||||
[store."fs"]
|
||||
type = "fs"
|
||||
path = "{TMP}"
|
||||
|
||||
[store."redis"]
|
||||
type = "redis"
|
||||
urls = "redis://127.0.0.1"
|
||||
redis-type = "single"
|
||||
|
||||
#[store."psql-replica"]
|
||||
#type = "sql-read-replica"
|
||||
#primary = "postgresql"
|
||||
#replicas = "postgresql"
|
||||
|
||||
[storage]
|
||||
data = "{STORE}"
|
||||
fts = "{SEARCH_STORE}"
|
||||
blob = "{BLOB_STORE}"
|
||||
lookup = "{LOOKUP_STORE}"
|
||||
directory = "{STORE}"
|
||||
|
||||
[directory."{STORE}"]
|
||||
type = "internal"
|
||||
store = "{STORE}"
|
||||
|
||||
[session.rcpt]
|
||||
directory = "'{STORE}'"
|
||||
|
||||
[session.auth]
|
||||
directory = "'{STORE}'"
|
||||
|
||||
"#;
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::store::cleanup::store_assert_is_empty;
|
||||
use crate::utils::{cleanup::store_assert_is_empty, server::TestServer};
|
||||
use ahash::AHashSet;
|
||||
use std::collections::HashSet;
|
||||
use store::Store;
|
||||
use store::write::RegistryClass;
|
||||
use store::{
|
||||
Store, ValueKey,
|
||||
ValueKey,
|
||||
rand::{self, Rng},
|
||||
write::{AlignedBytes, Archive, Archiver, BatchBuilder, MergeResult, Params, ValueClass},
|
||||
};
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
use types::collection::Collection;
|
||||
use types::collection::SyncCollection;
|
||||
|
||||
// FDB max value
|
||||
const MAX_VALUE_SIZE: usize = 100000;
|
||||
@@ -26,17 +29,17 @@ fn value_gen(chunks: impl IntoIterator<Item = (u8, usize)>) -> Vec<u8> {
|
||||
value
|
||||
}
|
||||
|
||||
pub async fn test(db: Store) {
|
||||
pub async fn test(test: &TestServer) {
|
||||
let db = test.server.store().clone();
|
||||
|
||||
#[cfg(feature = "foundationdb")]
|
||||
if matches!(db, Store::FoundationDb(_)) {
|
||||
use types::collection::Collection;
|
||||
|
||||
println!("Running FoundationDB chunked iterator test...");
|
||||
let kvs = [
|
||||
("a", value_gen([(b'a', 1)])),
|
||||
("b", value_gen([(b'b', MAX_VALUE_SIZE), (b'0', 1)])),
|
||||
(1, value_gen([(b'a', 1)])),
|
||||
(2, value_gen([(b'b', MAX_VALUE_SIZE), (b'0', 1)])),
|
||||
(
|
||||
"c",
|
||||
3,
|
||||
value_gen([
|
||||
(b'c', MAX_VALUE_SIZE),
|
||||
(b'1', MAX_VALUE_SIZE),
|
||||
@@ -44,10 +47,10 @@ pub async fn test(db: Store) {
|
||||
]),
|
||||
),
|
||||
(
|
||||
"d",
|
||||
4,
|
||||
value_gen([(b'd', MAX_VALUE_SIZE), (b'3', MAX_VALUE_SIZE)]),
|
||||
),
|
||||
("e", value_gen([(b'e', 1)])),
|
||||
(5, value_gen([(b'e', 1)])),
|
||||
];
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
@@ -56,7 +59,13 @@ pub async fn test(db: Store) {
|
||||
.with_document(0);
|
||||
|
||||
for (key, value) in &kvs {
|
||||
batch.set(ValueClass::Config(key.as_bytes().to_vec()), value.clone());
|
||||
batch.set(
|
||||
ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: *key,
|
||||
item_id: 0,
|
||||
}),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
db.write(batch.build_all()).await.unwrap();
|
||||
|
||||
@@ -68,13 +77,19 @@ pub async fn test(db: Store) {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Config(b"".to_vec()),
|
||||
class: ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: 0,
|
||||
item_id: 0,
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Config(b"\xFF".to_vec()),
|
||||
class: ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: u16::MAX,
|
||||
item_id: u64::MAX,
|
||||
}),
|
||||
},
|
||||
),
|
||||
|key, value| {
|
||||
@@ -92,13 +107,19 @@ pub async fn test(db: Store) {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Config(b"".to_vec()),
|
||||
class: ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: 0,
|
||||
item_id: 0,
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Config(b"\xFF".to_vec()),
|
||||
class: ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: u16::MAX,
|
||||
item_id: u64::MAX,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -114,7 +135,10 @@ pub async fn test(db: Store) {
|
||||
.with_document(0);
|
||||
for n in 0..900000 {
|
||||
batch.set(
|
||||
ValueClass::Config(format!("key{n:10}").into_bytes()),
|
||||
ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: 0,
|
||||
item_id: n,
|
||||
}),
|
||||
format!("value{n:10}").into_bytes(),
|
||||
);
|
||||
|
||||
@@ -139,13 +163,19 @@ pub async fn test(db: Store) {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Config(b"".to_vec()),
|
||||
class: ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: 0,
|
||||
item_id: 0,
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Config(b"\xFF".to_vec()),
|
||||
class: ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: 0,
|
||||
item_id: u64::MAX,
|
||||
}),
|
||||
},
|
||||
),
|
||||
|key, value| {
|
||||
@@ -169,7 +199,10 @@ pub async fn test(db: Store) {
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(0);
|
||||
for n in 0..900000 {
|
||||
batch.clear(ValueClass::Config(format!("key{n:10}").into_bytes()));
|
||||
batch.clear(ValueClass::Registry(RegistryClass::Item {
|
||||
object_id: 0,
|
||||
item_id: n,
|
||||
}));
|
||||
|
||||
if n % 10000 == 0 {
|
||||
db.write(batch.build_all()).await.unwrap();
|
||||
@@ -260,7 +293,7 @@ pub async fn test(db: Store) {
|
||||
.with_account_id(0)
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(0)
|
||||
.add_and_get(ValueClass::Directory(DirectoryClass::UsedQuota(0)), 1);
|
||||
.add_and_get(ValueClass::Quota, 1);
|
||||
db.write(builder.build_all())
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -283,7 +316,7 @@ pub async fn test(db: Store) {
|
||||
account_id: 0,
|
||||
collection: 0,
|
||||
document_id: 0,
|
||||
class: ValueClass::Directory(DirectoryClass::UsedQuota(0)),
|
||||
class: ValueClass::Quota,
|
||||
})
|
||||
.await
|
||||
.unwrap(),
|
||||
@@ -471,7 +504,7 @@ pub async fn test(db: Store) {
|
||||
.clear(ValueClass::Property(0))
|
||||
.clear(ValueClass::Property(2))
|
||||
.clear(ValueClass::Property(3))
|
||||
.clear(ValueClass::Directory(DirectoryClass::UsedQuota(0)))
|
||||
.clear(ValueClass::Quota)
|
||||
.clear(ValueClass::ChangeId);
|
||||
|
||||
for document_id in 0..1000 {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::store::deflate_test_resource;
|
||||
use crate::{store::deflate_test_resource, utils::server::TestServer};
|
||||
use ahash::AHashSet;
|
||||
use nlp::language::Language;
|
||||
use std::{
|
||||
@@ -105,7 +105,8 @@ const ALL_IDS: &[&str] = &[
|
||||
];
|
||||
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
pub async fn test(store: SearchStore, do_insert: bool) {
|
||||
pub async fn test(test: &TestServer, do_insert: bool) {
|
||||
let store = test.server.search_store().clone();
|
||||
println!("Running Store query tests...");
|
||||
|
||||
let pool = rayon::ThreadPoolBuilder::new()
|
||||
|
||||
@@ -259,7 +259,7 @@ pub async fn store_assert_is_empty(store: &Store, blob_store: BlobStore, include
|
||||
(SUBSPACE_REGISTRY_PK, true),
|
||||
(SUBSPACE_DIRECTORY, true),
|
||||
] {
|
||||
if (subspace == SUBSPACE_SEARCH_INDEX && store.is_pg_or_mysql())
|
||||
if subspace == SUBSPACE_SEARCH_INDEX && store.is_pg_or_mysql()
|
||||
//|| (subspace == directory && !include_directory)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -7,4 +7,6 @@
|
||||
pub mod account;
|
||||
pub mod cleanup;
|
||||
pub mod jmap;
|
||||
pub mod registry;
|
||||
pub mod server;
|
||||
pub mod storage;
|
||||
|
||||
163
tests/src/utils/registry.rs
Normal file
163
tests/src/utils/registry.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::{BlobStoreType, DataStoreType, InMemoryStoreType, SearchStoreType},
|
||||
prelude::Object,
|
||||
structs::{
|
||||
BlobStore, DataStore, ElasticSearchStore, FileSystemStore, FoundationDbStore, HttpAuth,
|
||||
HttpAuthBasic, InMemoryStore, MeilisearchStore, MySqlStore, PostgreSqlStore,
|
||||
RedisStore, RocksDbStore, S3Store, S3StoreCustomRegion, S3StoreRegion, SearchStore,
|
||||
SecretKey, SecretKeyOptional, SecretKeyValue, SqliteStore,
|
||||
},
|
||||
},
|
||||
types::{EnumImpl, duration::Duration},
|
||||
};
|
||||
use store::{
|
||||
RegistryStore,
|
||||
registry::write::{RegistryWrite, RegistryWriteResult},
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
pub trait RegistryEnvStores {
|
||||
fn insert_stores_from_env(&self) -> impl Future<Output = ()>;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
for store in [search_store, blob_store, in_memory].into_iter().flatten() {
|
||||
self.write(RegistryWrite::insert(&store))
|
||||
.await
|
||||
.expect("Failed to insert store into registry")
|
||||
.unwrap_id(trc::location!());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_data_store(typ: DataStoreType, path: &str) -> DataStore {
|
||||
match typ {
|
||||
DataStoreType::RocksDb => DataStore::RocksDb(RocksDbStore {
|
||||
path: format!("{path}/rocks.db"),
|
||||
..Default::default()
|
||||
}),
|
||||
DataStoreType::Sqlite => DataStore::Sqlite(SqliteStore {
|
||||
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: "postgres".to_string().into(),
|
||||
auth_secret: SecretKeyOptional::Value(SecretKeyValue {
|
||||
secret: "mysecretpassword".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()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_blob_store(typ: BlobStoreType, path: &str) -> BlobStore {
|
||||
match typ {
|
||||
BlobStoreType::S3 => BlobStore::S3(S3Store {
|
||||
access_key: "minioadmin".to_string().into(),
|
||||
bucket: "tmp".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()
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
SearchStoreType::Meilisearch => SearchStore::Meilisearch(MeilisearchStore {
|
||||
url: "http://localhost:7700".into(),
|
||||
allow_invalid_certs: true,
|
||||
poll_interval: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UnwrapRegistryId {
|
||||
fn unwrap_id(self, location: &str) -> Id;
|
||||
}
|
||||
|
||||
impl UnwrapRegistryId for RegistryWriteResult {
|
||||
fn unwrap_id(self, location: &str) -> Id {
|
||||
match self {
|
||||
RegistryWriteResult::Success(id) => id,
|
||||
err => panic!("Expected success at {location} but got {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
273
tests/src/utils/server.rs
Normal file
273
tests/src/utils/server.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
AssertConfig,
|
||||
store::TempDir,
|
||||
utils::{
|
||||
account::Account,
|
||||
cleanup::{search_store_destroy, store_destroy},
|
||||
registry::{RegistryEnvStores, UnwrapRegistryId, build_data_store},
|
||||
storage::assert_is_empty,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use common::{
|
||||
BuildServer, Caches, Core, Data, Inner, Server,
|
||||
config::{
|
||||
server::{Listeners, ServerProtocol},
|
||||
storage::Storage,
|
||||
telemetry::Telemetry,
|
||||
},
|
||||
manager::{boot::build_ipc, defaults::BootstrapDefaults},
|
||||
};
|
||||
use http::HttpSessionManager;
|
||||
use imap::core::ImapSessionManager;
|
||||
use managesieve::core::ManageSieveSessionManager;
|
||||
use pop3::Pop3SessionManager;
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::{DataStoreType, EventPolicy, NetworkListenerProtocol, TracingLevel},
|
||||
prelude::{Object, SocketAddr},
|
||||
structs::{NetworkListener, Tracer, TracerStdout},
|
||||
},
|
||||
types::{EnumImpl, map::Map},
|
||||
};
|
||||
use services::{SpawnServices, broadcast::subscriber::spawn_broadcast_subscriber};
|
||||
use smtp::{SpawnQueueManager, core::SmtpSessionManager};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use store::{
|
||||
RegistryStore, Store,
|
||||
registry::{bootstrap::Bootstrap, write::RegistryWrite},
|
||||
};
|
||||
use tokio::sync::watch;
|
||||
use trc::EventType;
|
||||
use types::id::Id;
|
||||
|
||||
pub struct TestServer {
|
||||
pub server: Server,
|
||||
accounts: AHashMap<&'static str, Account>,
|
||||
pub temp_dir: TempDir,
|
||||
shutdown_tx: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
pub struct TestServerBuilder {
|
||||
bootstrap: Bootstrap,
|
||||
temp_dir: TempDir,
|
||||
reset: bool,
|
||||
}
|
||||
|
||||
impl TestServerBuilder {
|
||||
pub async fn new(test_name: &str, reset: bool) -> Self {
|
||||
let temp_dir = TempDir::new(test_name, reset);
|
||||
let path = temp_dir.path.to_string_lossy().to_string();
|
||||
let data_store = build_data_store(
|
||||
std::env::var("STORE")
|
||||
.map(|store| DataStoreType::parse(&store).expect("Invalid store type"))
|
||||
.expect(concat!(
|
||||
"Missing or invalid store type. Try ",
|
||||
"running `STORE=<store_type> cargo test`"
|
||||
)),
|
||||
&path,
|
||||
);
|
||||
let store = Store::build(data_store).await.unwrap();
|
||||
|
||||
store.create_tables().await.unwrap();
|
||||
|
||||
// Delete old store if requested
|
||||
if reset {
|
||||
store_destroy(&store).await;
|
||||
}
|
||||
|
||||
Self {
|
||||
bootstrap: Bootstrap::new(
|
||||
RegistryStore::new(&path, store, "mail.example.org".to_string(), 1, None).await,
|
||||
)
|
||||
.await,
|
||||
temp_dir,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_listener(
|
||||
self,
|
||||
protocol: NetworkListenerProtocol,
|
||||
name: &str,
|
||||
port: u16,
|
||||
use_tls: bool,
|
||||
) -> Self {
|
||||
self.insert_object(NetworkListener {
|
||||
bind: Map::new(vec![SocketAddr::from_str(&format!("[::]:{port}")).unwrap()]),
|
||||
name: name.to_string(),
|
||||
protocol,
|
||||
use_tls,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn with_object(self, object: impl Into<Object>) -> Self {
|
||||
self.insert_object(object).await;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn insert_object(&self, object: impl Into<Object>) -> Id {
|
||||
self.bootstrap
|
||||
.registry
|
||||
.write(RegistryWrite::insert(&object.into()))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_id(trc::location!())
|
||||
}
|
||||
|
||||
pub async fn build(mut self) -> TestServer {
|
||||
// Register stores from environment
|
||||
self.bootstrap.registry.insert_stores_from_env().await;
|
||||
|
||||
// Enable logging if requested
|
||||
let level = std::env::var("LOG")
|
||||
.map(|log| TracingLevel::parse(&log).expect("Invalid log level"))
|
||||
.ok();
|
||||
self.bootstrap
|
||||
.registry
|
||||
.write(RegistryWrite::insert(
|
||||
&Tracer::Stdout(TracerStdout {
|
||||
enable: level.is_some(),
|
||||
level: level.unwrap_or(TracingLevel::Info),
|
||||
ansi: true,
|
||||
multiline: false,
|
||||
events: Map::new(
|
||||
EventType::variants()
|
||||
.iter()
|
||||
.filter(|ev| {
|
||||
let ev = ev.as_str();
|
||||
ev.starts_with("network.")
|
||||
|| ev == "telemetry.webhook-error"
|
||||
|| ev == "http.request-body"
|
||||
})
|
||||
.copied()
|
||||
.collect(),
|
||||
),
|
||||
events_policy: EventPolicy::Exclude,
|
||||
..Default::default()
|
||||
})
|
||||
.into(),
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_id(trc::location!());
|
||||
|
||||
// Start listeners
|
||||
let mut servers = Listeners::parse(&mut self.bootstrap).await;
|
||||
servers.bind_and_drop_priv(&mut self.bootstrap);
|
||||
|
||||
// Parse storage
|
||||
let storage = Storage::parse(&mut self.bootstrap).await;
|
||||
|
||||
// Reset search store
|
||||
if self.reset {
|
||||
search_store_destroy(&storage.search).await;
|
||||
}
|
||||
|
||||
// Parse telemetry
|
||||
let telemetry = Telemetry::parse(&mut self.bootstrap, &storage).await;
|
||||
|
||||
// Add safe defaults if missing
|
||||
self.bootstrap.insert_safe_defaults().await;
|
||||
|
||||
// Parse components
|
||||
let core = Box::pin(Core::parse(&mut self.bootstrap, storage)).await;
|
||||
let data = Data::parse(&mut self.bootstrap).await;
|
||||
let cache = Caches::parse(&mut self.bootstrap).await;
|
||||
|
||||
// Enable telemetry
|
||||
telemetry.enable(true);
|
||||
|
||||
// Build inner
|
||||
let (ipc, mut ipc_rxs) = build_ipc(!core.storage.coordinator.is_none());
|
||||
let inner = Arc::new(Inner {
|
||||
shared_core: core.into_shared(),
|
||||
data,
|
||||
ipc,
|
||||
cache,
|
||||
});
|
||||
|
||||
// Parse TCP acceptors
|
||||
servers
|
||||
.parse_tcp_acceptors(&mut self.bootstrap, inner.clone())
|
||||
.await;
|
||||
|
||||
// Start services
|
||||
self.bootstrap.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);
|
||||
|
||||
TestServer {
|
||||
server: inner.build_server(),
|
||||
temp_dir: self.temp_dir,
|
||||
accounts: Default::default(),
|
||||
shutdown_tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
pub fn account(&self, name: &str) -> &Account {
|
||||
self.accounts.get(name).unwrap()
|
||||
}
|
||||
|
||||
pub async fn assert_is_empty(&self) {
|
||||
assert_is_empty(&self.server).await;
|
||||
}
|
||||
|
||||
pub async fn destroy_store(&self) {
|
||||
store_destroy(self.server.store()).await;
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
let _ = self.shutdown_tx.send(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user