Push subscription improvements + CalendarAlert implementation (closes #1248)
This commit is contained in:
171
tests/src/jmap/calendar/alarm.rs
Normal file
171
tests/src/jmap/calendar/alarm.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use futures::StreamExt;
|
||||
use jmap_client::{
|
||||
CalendarAlert, PushObject, client_ws::WebSocketMessage, event_source::PushNotification,
|
||||
};
|
||||
use jmap_proto::request::method::MethodObject;
|
||||
use mail_parser::DateTime;
|
||||
use serde_json::json;
|
||||
use std::time::Instant;
|
||||
use store::write::now;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::jmap::{IntoJmapSet, JMAPTest, JmapUtils};
|
||||
|
||||
pub async fn test(params: &mut JMAPTest) {
|
||||
println!("Running Calendar Alarm tests...");
|
||||
let account = params.account("jdoe@example.com");
|
||||
let account_id = account.id_string();
|
||||
let client = account.client();
|
||||
let client_ws = account.client_owned().await;
|
||||
|
||||
// Create test calendar
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::Calendar,
|
||||
[json!({
|
||||
"name": "Alarming Calendar",
|
||||
})],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await;
|
||||
let calendar_id = response.created(0).id().to_string();
|
||||
|
||||
// Connect to EventSource
|
||||
let (event_tx, mut event_rx) = mpsc::channel::<PushNotification>(100);
|
||||
let mut notifications = client
|
||||
.event_source(None::<Vec<_>>, false, 1.into(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::spawn(async move {
|
||||
while let Some(notification) = notifications.next().await {
|
||||
if let Err(_err) = event_tx.send(notification.unwrap()).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Connect to WebSocket
|
||||
let mut ws_stream = client_ws.connect_ws().await.unwrap();
|
||||
let (stream_tx, mut stream_rx) = mpsc::channel::<WebSocketMessage>(100);
|
||||
tokio::spawn(async move {
|
||||
while let Some(change) = ws_stream.next().await {
|
||||
stream_tx.send(change.unwrap()).await.unwrap();
|
||||
}
|
||||
});
|
||||
client_ws
|
||||
.enable_push_ws(None::<Vec<_>>, None::<&str>)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create test event
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::CalendarEvent,
|
||||
[json!({
|
||||
"@type": "Event",
|
||||
"calendarIds": ([calendar_id.as_str()].into_jmap_set()),
|
||||
"description": "What mirror where?!",
|
||||
"timeZone": "Etc/UTC",
|
||||
"start": DateTime::from_timestamp(now() as i64 + 5)
|
||||
.to_rfc3339().trim_end_matches("Z").to_string(),
|
||||
"title": "See the pretty girl in that mirror there",
|
||||
"alerts": {
|
||||
"k1": {
|
||||
"@type": "Alert",
|
||||
"trigger": {
|
||||
"@type": "OffsetTrigger",
|
||||
"offset": "-PT2S"
|
||||
},
|
||||
"action": "display"
|
||||
},
|
||||
"k2": {
|
||||
"trigger": {
|
||||
"@type": "OffsetTrigger",
|
||||
"offset": "-PT4S"
|
||||
},
|
||||
"action": "display",
|
||||
"@type": "Alert"
|
||||
}
|
||||
},
|
||||
"locations": {
|
||||
"0b7168ae-ed3e-5eae-9540-89ba3a469b16": {
|
||||
"name": "West Side",
|
||||
"@type": "Location"
|
||||
}
|
||||
},
|
||||
"uid": "2371c2d9-a136-43b0-bba3-f6ab249ad46e",
|
||||
"duration": "P1D"
|
||||
})],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await;
|
||||
let event_id = response.created(0).id().to_string();
|
||||
|
||||
// Wait for alarm notifications
|
||||
let start = Instant::now();
|
||||
let mut ws_events = Vec::new();
|
||||
let mut es_events = Vec::new();
|
||||
|
||||
while start.elapsed().as_secs() < 7 && (ws_events.len() < 2 || es_events.len() < 2) {
|
||||
tokio::select! {
|
||||
Some(notification) = event_rx.recv() => {
|
||||
if let PushNotification::CalendarAlert(alert) = notification {
|
||||
es_events.push(alert);
|
||||
}
|
||||
}
|
||||
Some(message) = stream_rx.recv() => {
|
||||
match message {
|
||||
WebSocketMessage::PushNotification(PushObject::CalendarAlert(alert)) => {
|
||||
ws_events.push(alert);
|
||||
}
|
||||
WebSocketMessage::PushNotification(PushObject::Group {entries} ) => {
|
||||
ws_events.extend(entries.into_iter().filter_map(|entry| {
|
||||
if let PushObject::CalendarAlert(alert) = entry {
|
||||
Some(alert)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(6)) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expected_alerts = vec![
|
||||
CalendarAlert {
|
||||
account_id: account_id.to_string(),
|
||||
calendar_event_id: event_id.clone(),
|
||||
uid: "2371c2d9-a136-43b0-bba3-f6ab249ad46e".to_string(),
|
||||
recurrence_id: None,
|
||||
alert_id: "k2".to_string(),
|
||||
},
|
||||
CalendarAlert {
|
||||
account_id: account_id.to_string(),
|
||||
calendar_event_id: event_id.clone(),
|
||||
uid: "2371c2d9-a136-43b0-bba3-f6ab249ad46e".to_string(),
|
||||
recurrence_id: None,
|
||||
alert_id: "k1".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
es_events, expected_alerts,
|
||||
"EventSource alarms do not match"
|
||||
);
|
||||
assert_eq!(ws_events, expected_alerts, "WebSocket alarms do not match");
|
||||
|
||||
// Cleanup
|
||||
account.destroy_all_calendars().await;
|
||||
params.assert_is_empty().await;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
pub mod acl;
|
||||
pub mod alarm;
|
||||
pub mod calendars;
|
||||
pub mod event;
|
||||
pub mod identity;
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
use crate::jmap::{JMAPTest, mail::delivery::SmtpConnection};
|
||||
use email::mailbox::INBOX_ID;
|
||||
use futures::StreamExt;
|
||||
use jmap_client::{TypeState, event_source::Changes, mailbox::Role};
|
||||
use jmap_client::{
|
||||
DataType,
|
||||
event_source::{Changes, PushNotification},
|
||||
mailbox::Role,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use store::ahash::AHashSet;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -29,7 +33,13 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(change) = changes.next().await {
|
||||
if let Err(_err) = event_tx.send(change.unwrap()).await {
|
||||
if let Err(_err) = event_tx
|
||||
.send(match change.unwrap() {
|
||||
PushNotification::StateChange(changes) => changes,
|
||||
PushNotification::CalendarAlert(_) => unreachable!(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
//println!("Error sending event: {}", _err);
|
||||
break;
|
||||
}
|
||||
@@ -44,7 +54,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
assert_state(&mut event_rx, account.id_string(), &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut event_rx, account.id_string(), &[DataType::Mailbox]).await;
|
||||
|
||||
// Multiple changes should be grouped and delivered in intervals
|
||||
for num in 0..5 {
|
||||
@@ -53,7 +63,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
assert_state(&mut event_rx, account.id_string(), &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut event_rx, account.id_string(), &[DataType::Mailbox]).await;
|
||||
assert_ping(&mut event_rx).await; // Pings are only received in cfg(test)
|
||||
|
||||
// Ingest email and expect state change
|
||||
@@ -77,10 +87,10 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
&mut event_rx,
|
||||
account.id_string(),
|
||||
&[
|
||||
TypeState::EmailDelivery,
|
||||
TypeState::Email,
|
||||
TypeState::Thread,
|
||||
TypeState::Mailbox,
|
||||
DataType::EmailDelivery,
|
||||
DataType::Email,
|
||||
DataType::Thread,
|
||||
DataType::Mailbox,
|
||||
],
|
||||
)
|
||||
.await;
|
||||
@@ -88,7 +98,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
|
||||
// Destroy mailbox
|
||||
client.mailbox_destroy(&mailbox_id, true).await.unwrap();
|
||||
assert_state(&mut event_rx, account.id_string(), &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut event_rx, account.id_string(), &[DataType::Mailbox]).await;
|
||||
|
||||
// Destroy Inbox
|
||||
client
|
||||
@@ -98,7 +108,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
assert_state(
|
||||
&mut event_rx,
|
||||
account.id_string(),
|
||||
&[TypeState::Email, TypeState::Thread, TypeState::Mailbox],
|
||||
&[DataType::Email, DataType::Thread, DataType::Mailbox],
|
||||
)
|
||||
.await;
|
||||
assert_ping(&mut event_rx).await;
|
||||
@@ -111,7 +121,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
async fn assert_state(
|
||||
event_rx: &mut mpsc::Receiver<Changes>,
|
||||
account_id: &str,
|
||||
state: &[TypeState],
|
||||
state: &[DataType],
|
||||
) {
|
||||
match tokio::time::timeout(Duration::from_millis(700), event_rx.recv()).await {
|
||||
Ok(Some(changes)) => {
|
||||
@@ -120,8 +130,8 @@ async fn assert_state(
|
||||
.changes(account_id)
|
||||
.unwrap()
|
||||
.map(|x| x.0)
|
||||
.collect::<AHashSet<&TypeState>>(),
|
||||
state.iter().collect::<AHashSet<&TypeState>>()
|
||||
.collect::<AHashSet<&DataType>>(),
|
||||
state.iter().collect::<AHashSet<&DataType>>()
|
||||
);
|
||||
}
|
||||
result => {
|
||||
|
||||
@@ -12,7 +12,7 @@ use http_proto::{HtmlResponse, ToHttpResponse, request::fetch_body};
|
||||
use hyper::{StatusCode, body, header::CONTENT_ENCODING, server::conn::http1, service::service_fn};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use jmap_client::{mailbox::Role, push_subscription::Keys};
|
||||
use jmap_proto::response::status::StateChangeResponse;
|
||||
use jmap_proto::{response::status::PushObject, types::state::State};
|
||||
use services::state_manager::ece::ece_encrypt;
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -24,7 +24,7 @@ use std::{
|
||||
use store::ahash::AHashSet;
|
||||
use tokio::sync::mpsc;
|
||||
use types::{id::Id, type_state::DataType};
|
||||
use utils::config::Config;
|
||||
use utils::{config::Config, map::vec_map::VecMap};
|
||||
|
||||
const SERVER: &str = r#"
|
||||
[server]
|
||||
@@ -125,7 +125,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
|
||||
// Receive states just for the requested types
|
||||
client
|
||||
.push_subscription_update_types(&push_id, [jmap_client::TypeState::Email].into())
|
||||
.push_subscription_update_types(&push_id, [jmap_client::DataType::Email].into())
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
@@ -224,15 +224,15 @@ pub struct PushServer {
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
enum PushMessage {
|
||||
StateChange(StateChangeResponse),
|
||||
PushObject(PushObject),
|
||||
Verification(PushVerification),
|
||||
}
|
||||
|
||||
impl PushMessage {
|
||||
pub fn unwrap_state_change(self) -> StateChangeResponse {
|
||||
pub fn unwrap_state_change(self) -> VecMap<Id, VecMap<DataType, State>> {
|
||||
match self {
|
||||
PushMessage::StateChange(state_change) => state_change,
|
||||
_ => panic!("Expected StateChange"),
|
||||
PushMessage::PushObject(PushObject::StateChange { changed }) => changed,
|
||||
_ => panic!("Expected PushObject"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +348,6 @@ async fn assert_state(event_rx: &mut mpsc::Receiver<PushMessage>, id: &Id, state
|
||||
expect_push(event_rx)
|
||||
.await
|
||||
.unwrap_state_change()
|
||||
.changed
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::jmap::JMAPTest;
|
||||
use ahash::AHashSet;
|
||||
use futures::StreamExt;
|
||||
use jmap_client::{
|
||||
TypeState,
|
||||
DataType, PushObject,
|
||||
client_ws::WebSocketMessage,
|
||||
core::{
|
||||
response::{Response, TaggedMethodResponse},
|
||||
@@ -66,7 +66,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
.mailbox_update_sort_order(&mailbox_id, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_state(&mut stream_rx, account.id_string(), &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut stream_rx, account.id_string(), &[DataType::Mailbox]).await;
|
||||
|
||||
// Multiple changes should be grouped and delivered in intervals
|
||||
for num in 0..5 {
|
||||
@@ -76,7 +76,7 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
.unwrap();
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_state(&mut stream_rx, account.id_string(), &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut stream_rx, account.id_string(), &[DataType::Mailbox]).await;
|
||||
expect_nothing(&mut stream_rx).await;
|
||||
|
||||
// Disable push notifications
|
||||
@@ -117,18 +117,18 @@ async fn expect_response(
|
||||
async fn assert_state(
|
||||
stream_rx: &mut mpsc::Receiver<WebSocketMessage>,
|
||||
id: &str,
|
||||
state: &[TypeState],
|
||||
state: &[DataType],
|
||||
) {
|
||||
match tokio::time::timeout(Duration::from_millis(700), stream_rx.recv()).await {
|
||||
Ok(Some(message)) => match message {
|
||||
WebSocketMessage::StateChange(changes) => {
|
||||
WebSocketMessage::PushNotification(PushObject::StateChange { changed }) => {
|
||||
assert_eq!(
|
||||
changes
|
||||
.changes(id)
|
||||
changed
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.map(|x| x.0)
|
||||
.collect::<AHashSet<&TypeState>>(),
|
||||
state.iter().collect::<AHashSet<&TypeState>>()
|
||||
.keys()
|
||||
.collect::<AHashSet<&DataType>>(),
|
||||
state.iter().collect::<AHashSet<&DataType>>()
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected state change, got: {:?}", message),
|
||||
|
||||
@@ -78,11 +78,12 @@ async fn jmap_tests() {
|
||||
)
|
||||
.await;
|
||||
|
||||
/*server::webhooks::test(&mut params).await;
|
||||
mail::query::test(&mut params, delete).await;
|
||||
server::webhooks::test(&mut params).await;
|
||||
|
||||
mail::get::test(&mut params).await;
|
||||
mail::set::test(&mut params).await;
|
||||
mail::parse::test(&mut params).await;
|
||||
mail::query::test(&mut params, delete).await;
|
||||
mail::search_snippet::test(&mut params).await;
|
||||
mail::changes::test(&mut params).await;
|
||||
mail::query_changes::test(&mut params).await;
|
||||
@@ -92,20 +93,20 @@ async fn jmap_tests() {
|
||||
mail::mailbox::test(&mut params).await;
|
||||
mail::delivery::test(&mut params).await;
|
||||
mail::acl::test(&mut params).await;
|
||||
auth::limits::test(&mut params).await;
|
||||
auth::oauth::test(&mut params).await;
|
||||
core::event_source::test(&mut params).await;
|
||||
core::push_subscription::test(&mut params).await;
|
||||
mail::sieve_script::test(&mut params).await;
|
||||
mail::vacation_response::test(&mut params).await;
|
||||
mail::submission::test(&mut params).await;
|
||||
core::websocket::test(&mut params).await;
|
||||
auth::quota::test(&mut params).await;
|
||||
mail::crypto::test(&mut params).await;
|
||||
|
||||
core::event_source::test(&mut params).await;
|
||||
core::websocket::test(&mut params).await;
|
||||
core::push_subscription::test(&mut params).await;
|
||||
core::blob::test(&mut params).await;
|
||||
|
||||
auth::limits::test(&mut params).await;
|
||||
auth::oauth::test(&mut params).await;
|
||||
auth::quota::test(&mut params).await;
|
||||
auth::permissions::test(¶ms).await;
|
||||
server::purge::test(&mut params).await;
|
||||
server::enterprise::test(&mut params).await;*/
|
||||
|
||||
contacts::addressbook::test(&mut params).await;
|
||||
contacts::contact::test(&mut params).await;
|
||||
@@ -117,12 +118,17 @@ async fn jmap_tests() {
|
||||
calendar::calendars::test(&mut params).await;
|
||||
calendar::event::test(&mut params).await;
|
||||
calendar::notification::test(&mut params).await;
|
||||
calendar::alarm::test(&mut params).await;
|
||||
|
||||
calendar::identity::test(&mut params).await;
|
||||
calendar::acl::test(&mut params).await;
|
||||
|
||||
principal::get::test(&mut params).await;
|
||||
principal::availability::test(&mut params).await;
|
||||
|
||||
server::purge::test(&mut params).await;
|
||||
server::enterprise::test(&mut params).await;
|
||||
|
||||
if delete {
|
||||
params.temp_dir.delete();
|
||||
}
|
||||
@@ -1691,6 +1697,9 @@ enable = true
|
||||
[sharing]
|
||||
allow-directory-query = true
|
||||
|
||||
[calendar.alarms]
|
||||
minimum-interval = "1s"
|
||||
|
||||
[tracer.console]
|
||||
type = "console"
|
||||
level = "{LEVEL}"
|
||||
|
||||
Reference in New Issue
Block a user