Registry testing - part 9
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use email::mailbox::{DRAFTS_ID, INBOX_ID, JUNK_ID};
|
||||
use store::write::now;
|
||||
use types::{id::Id, keyword::Keyword};
|
||||
|
||||
use crate::{imap::antispam::*, jmap::JMAPTest};
|
||||
|
||||
pub async fn test(test: &mut TestServer) {
|
||||
println!("Running Email Spam classifier tests...");
|
||||
let account = test.account("jdoe@example.com");
|
||||
let client = account.jmap_client().await;
|
||||
let account_id = account.id().document_id();
|
||||
|
||||
// Make sure there are no training samples
|
||||
spam_delete_samples(¶ms.server).await;
|
||||
assert_eq!(spam_training_samples(¶ms.server).await.total_count, 0);
|
||||
|
||||
// Import samples
|
||||
let mut spam_ids = vec![];
|
||||
let mut ham_ids = vec![];
|
||||
for (idx, samples) in [&SPAM, &HAM].into_iter().enumerate() {
|
||||
let is_spam = idx == 0;
|
||||
for (num, sample) in samples.iter().enumerate() {
|
||||
let mut mailbox_ids = vec![];
|
||||
let mut keywords = vec![];
|
||||
|
||||
if num == 0 {
|
||||
if is_spam {
|
||||
mailbox_ids.push(Id::from(JUNK_ID).to_string());
|
||||
keywords.push(Keyword::Junk.to_string());
|
||||
} else {
|
||||
mailbox_ids.push(Id::from(INBOX_ID).to_string());
|
||||
keywords.push(Keyword::NotJunk.to_string());
|
||||
}
|
||||
} else {
|
||||
mailbox_ids.push(Id::from(DRAFTS_ID).to_string());
|
||||
}
|
||||
|
||||
let mail_id = client
|
||||
.email_import(
|
||||
sample.as_bytes().to_vec(),
|
||||
&mailbox_ids,
|
||||
Some(&keywords),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
if is_spam {
|
||||
spam_ids.push(mail_id);
|
||||
} else {
|
||||
ham_ids.push(mail_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let samples = spam_training_samples(¶ms.server).await;
|
||||
assert_eq!(samples.ham_count, 1);
|
||||
assert_eq!(samples.spam_count, 1);
|
||||
|
||||
// Train the classifier via JMAP
|
||||
for (ids, is_spam) in [(&spam_ids, true), (&ham_ids, false)] {
|
||||
for (idx, id) in ids.iter().skip(1).enumerate() {
|
||||
// Set keywords and mailboxes
|
||||
let mut request = client.build();
|
||||
let req = request.set_email().update(id);
|
||||
if idx < 5 || !is_spam {
|
||||
// Update via keywords
|
||||
let keyword = if is_spam {
|
||||
Keyword::Junk
|
||||
} else {
|
||||
Keyword::NotJunk
|
||||
}
|
||||
.to_string();
|
||||
req.keywords([&keyword]);
|
||||
} else {
|
||||
// Update via mailbox
|
||||
let mailbox_id = if is_spam { JUNK_ID } else { INBOX_ID };
|
||||
req.mailbox_ids([&Id::from(mailbox_id).to_string()]);
|
||||
}
|
||||
|
||||
request.send_set_email().await.unwrap().updated(id).unwrap();
|
||||
}
|
||||
}
|
||||
let samples = spam_training_samples(¶ms.server).await;
|
||||
assert_eq!(samples.ham_count, 10);
|
||||
assert_eq!(samples.spam_count, 10);
|
||||
|
||||
// Reclassifying an email should not add a new sample
|
||||
let mut request = client.build();
|
||||
request
|
||||
.set_email()
|
||||
.update(&ham_ids[0])
|
||||
.keywords([Keyword::Junk.to_string()]);
|
||||
request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.updated(&ham_ids[0])
|
||||
.unwrap();
|
||||
let samples = spam_training_samples(¶ms.server).await;
|
||||
assert_eq!(samples.ham_count, 9);
|
||||
assert_eq!(samples.spam_count, 11);
|
||||
assert_eq!(samples.samples.len(), 20);
|
||||
let hold_for = params
|
||||
.server
|
||||
.core
|
||||
.spam
|
||||
.classifier
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.hold_samples_for;
|
||||
assert!(hold_for > 2 * 86400);
|
||||
let hold_until = now() + hold_for;
|
||||
let hold_range = (hold_until - 86400)..=hold_until;
|
||||
assert!(samples.samples.iter().all(|s| s.account_id == account_id
|
||||
&& s.remove.is_none()
|
||||
&& hold_range.contains(&s.until)));
|
||||
|
||||
// Purging blobs should not remove training samples
|
||||
params
|
||||
.server
|
||||
.store()
|
||||
.purge_blobs(params.server.blob_store().clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let samples = spam_training_samples(¶ms.server).await;
|
||||
assert_eq!(samples.ham_count, 9);
|
||||
assert_eq!(samples.spam_count, 11);
|
||||
assert_eq!(samples.samples.len(), 20);
|
||||
|
||||
// Extend hold period so a new training sample is generated
|
||||
let old_core = params.server.core.clone();
|
||||
let mut new_core = old_core.as_ref().clone();
|
||||
new_core.spam.classifier.as_mut().unwrap().hold_samples_for += 2 * 86400;
|
||||
params.server.inner.shared_core.store(Arc::new(new_core));
|
||||
|
||||
// Reclassifying an email will now add a new sample
|
||||
let mut request = client.build();
|
||||
request
|
||||
.set_email()
|
||||
.update(&ham_ids[0])
|
||||
.keywords([Keyword::NotJunk.to_string()]);
|
||||
request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.updated(&ham_ids[0])
|
||||
.unwrap();
|
||||
let samples = spam_training_samples(¶ms.server).await;
|
||||
assert_eq!(samples.ham_count, 10);
|
||||
assert_eq!(samples.spam_count, 11);
|
||||
assert_eq!(samples.samples.len(), 21);
|
||||
|
||||
// Blob purge should remove the duplicated sample
|
||||
params
|
||||
.server
|
||||
.store()
|
||||
.purge_blobs(params.server.blob_store().clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let samples = spam_training_samples(¶ms.server).await;
|
||||
assert_eq!(samples.ham_count, 10);
|
||||
assert_eq!(samples.spam_count, 10);
|
||||
assert_eq!(samples.samples.len(), 20);
|
||||
|
||||
test.destroy_all_mailboxes(account).await;
|
||||
test.assert_is_empty().await;;
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::jmap::{JMAPTest, ManagementApi, mail::delivery::SmtpConnection};
|
||||
use mail_parser::{MessageParser, MimeHeaders};
|
||||
use std::path::PathBuf;
|
||||
use store::{
|
||||
Deserialize, Serialize,
|
||||
write::{Archive, Archiver},
|
||||
};
|
||||
|
||||
pub async fn test(test: &mut TestServer) {
|
||||
println!("Running Encryption-at-rest tests...");
|
||||
|
||||
// Check encryption
|
||||
check_is_encrypted();
|
||||
import_certs_and_encrypt().await;
|
||||
|
||||
// Create test account
|
||||
let account = test.account("jdoe@example.com");
|
||||
let client = account.jmap_client().await;
|
||||
|
||||
// Build API
|
||||
let api = ManagementApi::new(8899, "jdoe@example.com", "12345");
|
||||
|
||||
// Try importing using multiple methods and symmetric algos
|
||||
for (file_name, method, num_certs) in [
|
||||
("cert_smime.pem", EncryptionMethod::SMIME, 3),
|
||||
("cert_pgp.pem", EncryptionMethod::PGP, 1),
|
||||
] {
|
||||
let certs = std::fs::read_to_string(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("crypto")
|
||||
.join(file_name),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for algo in [Algorithm::Aes128, Algorithm::Aes256] {
|
||||
let request = match method {
|
||||
EncryptionMethod::PGP => EncryptionType::PGP {
|
||||
algo,
|
||||
certs: certs.clone(),
|
||||
allow_spam_training: true,
|
||||
},
|
||||
EncryptionMethod::SMIME => EncryptionType::SMIME {
|
||||
algo,
|
||||
certs: certs.clone(),
|
||||
allow_spam_training: true,
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
api.post::<u32>("/api/account/crypto", &request)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data(),
|
||||
num_certs
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Send a new message, which should be encrypted
|
||||
let mut lmtp = SmtpConnection::connect().await;
|
||||
lmtp.ingest(
|
||||
"bill@example.com",
|
||||
&["jdoe@example.com"],
|
||||
concat!(
|
||||
"From: bill@example.com\r\n",
|
||||
"To: jdoe@example.com\r\n",
|
||||
"Subject: TPS Report (should be encrypted)\r\n",
|
||||
"\r\n",
|
||||
"I'm going to need those TPS reports ASAP. ",
|
||||
"So, if you could do that, that'd be great."
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Send an encrypted message
|
||||
lmtp.ingest(
|
||||
"bill@example.com",
|
||||
&["jdoe@example.com"],
|
||||
concat!(
|
||||
"From: bill@example.com\r\n",
|
||||
"To: jdoe@example.com\r\n",
|
||||
"Subject: TPS Report (already encrypted)\r\n",
|
||||
"Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\r\n",
|
||||
"\r\n",
|
||||
"xjMEZMYfNhYJKwYBBAHaRw8BAQdAYyTN1HzqapLw8xwkCGwa0OjsgT/JqhcB/+Dy",
|
||||
"Ga1fsBrNG0pvaG4gRG9lIDxqb2huQGV4YW1wbGUub3JnPsKJBBMWCAAxFiEEg836",
|
||||
"pwbXpuQ/THMtpJwd4oBfIrUFAmTGHzYCGwMECwkIBwUVCAkKCwUWAgMBAAAKCRCk",
|
||||
"nB3igF8itYhyAQD2jEdeYa3gyQ47X9YWZTK1wEJkN8W9//V1fYl2XQwqlQEA0qBv",
|
||||
"Ai6nUh99oDw+/zQ8DFIKdeb5Ti4tu/X58PdpiQ7OOARkxh82EgorBgEEAZdVAQUB",
|
||||
"AQdAvXz2FbFN0DovQF/ACnZyczTsSIQp0mvmF1PE+aijbC8DAQgHwngEGBYIACAW",
|
||||
"IQSDzfqnBtem5D9Mcy2knB3igF8itQUCZMYfNgIbDAAKCRCknB3igF8itRnoAQC3",
|
||||
"GzPmgx7TnB+SexPuJV/DoKSMJ0/X+hbEFcZkulxaDQEAh+xiJCvf+ZNAKw6kFhsL",
|
||||
"UuZhEDktxnY6Ehz3aB7FawA=",
|
||||
"=KGrr",
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Disable encryption
|
||||
assert_eq!(
|
||||
api.post::<Option<String>>("/api/account/crypto", &EncryptionType::Disabled)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data(),
|
||||
None
|
||||
);
|
||||
|
||||
// Send a new message, which should NOT be encrypted
|
||||
lmtp.ingest(
|
||||
"bill@example.com",
|
||||
&["jdoe@example.com"],
|
||||
concat!(
|
||||
"From: bill@example.com\r\n",
|
||||
"To: jdoe@example.com\r\n",
|
||||
"Subject: TPS Report (plain text)\r\n",
|
||||
"\r\n",
|
||||
"I'm going to need those TPS reports ASAP. ",
|
||||
"So, if you could do that, that'd be great."
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Check messages
|
||||
let mut request = client.build();
|
||||
request.get_email();
|
||||
let emails = request.send_get_email().await.unwrap().take_list();
|
||||
assert_eq!(emails.len(), 3, "3 messages were expected: {:#?}.", emails);
|
||||
|
||||
for email in emails {
|
||||
let message =
|
||||
String::from_utf8(client.download(email.blob_id().unwrap()).await.unwrap()).unwrap();
|
||||
if message.contains("should be encrypted") {
|
||||
assert!(
|
||||
message.contains("Content-Type: multipart/encrypted"),
|
||||
"got message {message}, expected encrypted message"
|
||||
);
|
||||
} else if message.contains("already encrypted") {
|
||||
assert!(
|
||||
message.contains("Content-Type: application/pkcs7-mime")
|
||||
&& message.contains("xjMEZMYfNhYJKwYBBAHaRw8BAQdAYy"),
|
||||
"got message {message}, expected message to be left intact"
|
||||
);
|
||||
} else if message.contains("plain text") {
|
||||
assert!(
|
||||
message.contains("I'm going to need those TPS reports ASAP."),
|
||||
"got message {message}, expected plain text message"
|
||||
);
|
||||
} else {
|
||||
panic!("Unexpected message: {:#?}", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn import_certs_and_encrypt() {
|
||||
for (name, method, expected_certs) in [
|
||||
("cert_pgp.pem", EncryptionMethod::PGP, 1),
|
||||
//("cert_pgp.der", EncryptionMethod::PGP, 1),
|
||||
("cert_smime.pem", EncryptionMethod::SMIME, 3),
|
||||
("cert_smime.der", EncryptionMethod::SMIME, 1),
|
||||
] {
|
||||
let mut certs = try_parse_certs(
|
||||
method,
|
||||
std::fs::read(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("crypto")
|
||||
.join(name),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.expect(name);
|
||||
|
||||
assert_eq!(certs.len(), expected_certs);
|
||||
|
||||
if method == EncryptionMethod::PGP && certs.len() == 2 {
|
||||
// PGP library won't encrypt using EC
|
||||
let mut certs_ = certs.to_vec();
|
||||
certs_.pop();
|
||||
certs = certs_.into();
|
||||
}
|
||||
|
||||
let mut params = EncryptionParams {
|
||||
certs,
|
||||
flags: method.flags(),
|
||||
};
|
||||
|
||||
for algo in [Algorithm::Aes128, Algorithm::Aes256] {
|
||||
let message = MessageParser::new()
|
||||
.parse(b"Subject: test\r\ntest\r\n")
|
||||
.unwrap();
|
||||
assert!(!message.is_encrypted());
|
||||
params.flags = algo.flags() | method.flags();
|
||||
let arch =
|
||||
Archive::deserialize_owned(Archiver::new(params.clone()).serialize().unwrap())
|
||||
.unwrap();
|
||||
message
|
||||
.encrypt(arch.unarchive::<EncryptionParams>().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// S/MIME and PGP should not be allowed mixed
|
||||
assert!(
|
||||
try_parse_certs(
|
||||
EncryptionMethod::PGP,
|
||||
std::fs::read(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("crypto")
|
||||
.join("cert_mixed.pem"),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn check_is_encrypted() {
|
||||
let messages = std::fs::read_to_string(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("crypto")
|
||||
.join("is_encrypted.txt"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for raw_message in messages.split("!!!") {
|
||||
let is_encrypted = raw_message.contains("TRUE");
|
||||
let message = MessageParser::new()
|
||||
.parse(raw_message.trim().as_bytes())
|
||||
.unwrap();
|
||||
assert!(message.content_type().is_some());
|
||||
assert_eq!(
|
||||
message.is_encrypted(),
|
||||
is_encrypted,
|
||||
"failed for {raw_message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -70,13 +70,6 @@ message = "this should not have happened"
|
||||
|
||||
"#;
|
||||
|
||||
const RAW_MESSAGE: &str = "From: john@example.com
|
||||
To: john@example.com
|
||||
Subject: undelete test
|
||||
|
||||
test
|
||||
";
|
||||
|
||||
pub async fn test(test: &mut TestServer) {
|
||||
// Enable Enterprise
|
||||
println!("Running Enterprise tests...");
|
||||
@@ -166,35 +159,6 @@ pub async fn test(test: &mut TestServer) {
|
||||
);
|
||||
}
|
||||
|
||||
pub trait EnterpriseCore {
|
||||
fn enable_enterprise(self) -> Self;
|
||||
}
|
||||
|
||||
impl EnterpriseCore for Core {
|
||||
fn enable_enterprise(mut self) -> Self {
|
||||
self.enterprise = Enterprise {
|
||||
license: LicenseKey {
|
||||
valid_to: now() + 3600,
|
||||
valid_from: now() - 3600,
|
||||
domain: String::new(),
|
||||
accounts: 100,
|
||||
},
|
||||
undelete: None,
|
||||
trace_store: None,
|
||||
metrics_store: None,
|
||||
metrics_alerts: vec![],
|
||||
logo_url: None,
|
||||
ai_apis: Default::default(),
|
||||
spam_filter_llm: None,
|
||||
template_calendar_alarm: None,
|
||||
template_scheduling_email: None,
|
||||
template_scheduling_web: None,
|
||||
}
|
||||
.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
async fn alerts(server: &Server) {
|
||||
// Make sure the required metrics are set to 0
|
||||
assert_eq!(
|
||||
@@ -384,111 +348,6 @@ async fn metrics(test: &mut TestServer) {
|
||||
);
|
||||
}
|
||||
|
||||
async fn undelete(test: &mut TestServer) {
|
||||
// Authenticate
|
||||
let mut imap = ImapConnection::connect(b"_x ").await;
|
||||
imap.authenticate("jdoe@example.com", "12345").await;
|
||||
|
||||
// Insert test message
|
||||
imap.send("STATUS INBOX (MESSAGES)").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MESSAGES 0");
|
||||
imap.send(&format!("APPEND INBOX {{{}}}", RAW_MESSAGE.len()))
|
||||
.await;
|
||||
imap.assert_read(Type::Continuation, ResponseType::Ok).await;
|
||||
imap.send_untagged(RAW_MESSAGE).await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
// Make sure the message is there
|
||||
imap.send("STATUS INBOX (MESSAGES)").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MESSAGES 1");
|
||||
imap.send("SELECT INBOX").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
// Fetch message body
|
||||
imap.send("FETCH 1 BODY[]").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("Subject: undelete test");
|
||||
|
||||
// Delete and expunge message
|
||||
imap.send("STORE 1 +FLAGS (\\Deleted)").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
imap.send("EXPUNGE").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
// Logout and reconnect
|
||||
imap.send("LOGOUT").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
let mut imap = ImapConnection::connect(b"_x ").await;
|
||||
imap.authenticate("jdoe@example.com", "12345").await;
|
||||
|
||||
// Make sure the message is gone
|
||||
imap.send("STATUS INBOX (MESSAGES)").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MESSAGES 0");
|
||||
|
||||
// Query undelete API
|
||||
let api = ManagementApi::new(8899, "admin", "secret");
|
||||
api.get::<serde_json::Value>("/api/store/purge/account/jdoe@example.com")
|
||||
.await
|
||||
.unwrap();
|
||||
test.wait_for_tasks().await;
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let deleted = api
|
||||
.get::<List<DeletedBlobResponse>>("/api/store/undelete/jdoe@example.com")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
.items;
|
||||
assert_eq!(deleted.len(), 1);
|
||||
let deleted = deleted.into_iter().next().unwrap();
|
||||
match deleted.item {
|
||||
DeletedItemResponse::Email { from, subject, .. } => {
|
||||
assert_eq!(subject.as_ref(), "undelete test");
|
||||
assert_eq!(from.as_ref(), "john@example.com");
|
||||
}
|
||||
other => {
|
||||
panic!("Unexpected deleted item response: {:?}", other);
|
||||
}
|
||||
}
|
||||
|
||||
// Undelete
|
||||
let result = api
|
||||
.post::<Vec<UndeleteResponse>>(
|
||||
"/api/store/undelete/jdoe@example.com",
|
||||
&vec![UndeleteRequest {
|
||||
hash: deleted.hash,
|
||||
collection: "email".to_string(),
|
||||
time: deleted.deleted_at,
|
||||
cancel_deletion: deleted.expires_at.into(),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
assert_eq!(result, vec![UndeleteResponse::Success]);
|
||||
|
||||
// Make sure the message is back
|
||||
imap.send("STATUS INBOX (MESSAGES)").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MESSAGES 1");
|
||||
|
||||
imap.send("SELECT INBOX").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||
|
||||
// Fetch message body
|
||||
imap.send("FETCH 1 BODY[]").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("Subject: undelete test");
|
||||
}
|
||||
|
||||
pub async fn insert_test_metrics(core: Arc<Core>) {
|
||||
let store = core.storage.data.clone();
|
||||
store.purge_metrics(Duration::from_secs(0)).await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user