635 lines
23 KiB
Rust
635 lines
23 KiB
Rust
//! End-to-end test against a running ActivePush with the dev backends
|
|
//! (`tools/dev/test-backends.sh` + `tools/dev/test-settings.json`).
|
|
//!
|
|
//! AP_E2E_URL=http://127.0.0.1:18081 cargo test --test e2e -- --nocapture --test-threads=1
|
|
//!
|
|
//! Skipped when AP_E2E_URL is not set.
|
|
|
|
use std::time::Duration;
|
|
|
|
use active_push::backend::imap::ImapConn;
|
|
use active_push::config::{ImapSettings, TlsMode};
|
|
use active_push::probe::Probe;
|
|
use active_push::wbxml::Element;
|
|
|
|
const USER: &str = "jan@test.local";
|
|
const PASS: &str = "Test-Password-123";
|
|
|
|
fn url() -> Option<String> {
|
|
std::env::var("AP_E2E_URL")
|
|
.ok()
|
|
.map(|u| format!("{}/Microsoft-Server-ActiveSync", u.trim_end_matches('/')))
|
|
}
|
|
|
|
async fn imap() -> ImapConn {
|
|
let cfg = ImapSettings {
|
|
host: "127.0.0.1".into(),
|
|
port: 11143,
|
|
security: TlsMode::None,
|
|
..Default::default()
|
|
};
|
|
let mut c = ImapConn::connect(&cfg).await.unwrap();
|
|
assert!(c.login(USER, PASS).await.unwrap());
|
|
c
|
|
}
|
|
|
|
struct Folders {
|
|
list: Vec<(String, String, String, String)>, // id, parent, name, type
|
|
}
|
|
|
|
impl Folders {
|
|
fn by_type(&self, t: &str) -> String {
|
|
self.list
|
|
.iter()
|
|
.find(|f| f.3 == t)
|
|
.map(|f| f.0.clone())
|
|
.unwrap_or_else(|| panic!("no folder of type {t}"))
|
|
}
|
|
fn by_name(&self, n: &str) -> (String, String) {
|
|
self.list
|
|
.iter()
|
|
.find(|f| f.2 == n)
|
|
.map(|f| (f.0.clone(), f.1.clone()))
|
|
.unwrap_or_else(|| panic!("no folder {n}"))
|
|
}
|
|
}
|
|
|
|
fn col(r: &Element) -> &Element {
|
|
r.path(&["AirSync:Collections", "AirSync:Collection"])
|
|
.expect("collection in sync response")
|
|
}
|
|
|
|
async fn full_sync(p: &Probe, id: &str) -> (String, Vec<Element>) {
|
|
let r = p.sync(id, "0", 0, None).await.unwrap();
|
|
let mut key = col(&r).child_text("AirSync:SyncKey").unwrap();
|
|
let mut items = Vec::new();
|
|
for _ in 0..50 {
|
|
let r = p.sync(id, &key, 100, None).await.unwrap();
|
|
let c = col(&r);
|
|
assert_eq!(
|
|
c.child_text("AirSync:Status").as_deref(),
|
|
Some("1"),
|
|
"sync status: {}",
|
|
r.to_xml()
|
|
);
|
|
key = c.child_text("AirSync:SyncKey").unwrap();
|
|
if let Some(cmds) = c.child("AirSync:Commands") {
|
|
items.extend(cmds.children.iter().cloned());
|
|
}
|
|
if !c.has("AirSync:MoreAvailable") {
|
|
break;
|
|
}
|
|
}
|
|
(key, items)
|
|
}
|
|
|
|
async fn changes(p: &Probe, id: &str, key: &str, cmds: Option<Element>) -> (String, Element) {
|
|
let r = p.sync(id, key, 100, cmds).await.unwrap();
|
|
let c = col(&r).clone();
|
|
(c.child_text("AirSync:SyncKey").unwrap(), c)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn e2e_full_flow() {
|
|
let Some(url) = url() else {
|
|
eprintln!("AP_E2E_URL not set - skipping end-to-end test");
|
|
return;
|
|
};
|
|
let dev = format!("e2e{}", std::process::id());
|
|
// seed fresh messages with an attachment
|
|
{
|
|
let mut im = imap().await;
|
|
for f in ["Zmieniony", "Nowy folder &AXw-"] {
|
|
let _ = im.delete(f).await;
|
|
}
|
|
for i in 1..=3 {
|
|
let m = format!("From: Anna Nowak <anna@test.local>\r\nTo: Jan <jan@test.local>\r\nSubject: Testowa wiadomość e2e {i}\r\nMessage-ID: <e2e{i}.{dev}@test.local>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=b1\r\n\r\n--b1\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nTreść wiadomości numer {i}.\r\n--b1\r\nContent-Type: application/pdf; name=\"raport.pdf\"\r\nContent-Disposition: attachment; filename=\"raport.pdf\"\r\nContent-Transfer-Encoding: base64\r\n\r\nJVBERi0xLjQKJcOkw7zDtsOfCg==\r\n--b1--\r\n");
|
|
im.append("INBOX", "", m.as_bytes()).await.unwrap();
|
|
}
|
|
}
|
|
let mut p = Probe::new(&url, USER, PASS, &dev, true).unwrap();
|
|
|
|
// ---- auth failure gives 401
|
|
let bad = Probe::new(&url, USER, "wrong-password", &dev, true).unwrap();
|
|
assert_eq!(bad.options().await.unwrap().0, 401);
|
|
|
|
// ---- provisioning is enforced
|
|
let (st, _) = p
|
|
.cmd(
|
|
"FolderSync",
|
|
Some(
|
|
Element::new("FolderHierarchy:FolderSync")
|
|
.with_text("FolderHierarchy:SyncKey", "0"),
|
|
),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
st == 449 || st == 200,
|
|
"unprovisioned FolderSync answered {st}"
|
|
);
|
|
p.provision().await.unwrap();
|
|
|
|
// ---- hierarchy
|
|
let fs = p.folder_sync("0").await.unwrap();
|
|
let fkey = fs.child_text("FolderHierarchy:SyncKey").unwrap();
|
|
let mut list = Vec::new();
|
|
for a in fs
|
|
.child("FolderHierarchy:Changes")
|
|
.unwrap()
|
|
.children_named("FolderHierarchy:Add")
|
|
{
|
|
list.push((
|
|
a.child_text("FolderHierarchy:ServerId").unwrap(),
|
|
a.child_text("FolderHierarchy:ParentId").unwrap(),
|
|
a.child_text("FolderHierarchy:DisplayName").unwrap(),
|
|
a.child_text("FolderHierarchy:Type").unwrap(),
|
|
));
|
|
}
|
|
let f = Folders { list };
|
|
let inbox = f.by_type("2");
|
|
let trash = f.by_type("4");
|
|
let sent = f.by_type("5");
|
|
let cal = f.by_type("8");
|
|
let tasks = f.by_type("7");
|
|
let contacts = f.by_type("9");
|
|
let (projekty, _) = f.by_name("Projekty");
|
|
let (_, alfa_parent) = f.by_name("Alfa");
|
|
assert_eq!(alfa_parent, projekty, "subfolder parent");
|
|
// unchanged hierarchy keeps key
|
|
let fs2 = p.folder_sync(&fkey).await.unwrap();
|
|
assert_eq!(
|
|
fs2.path(&["FolderHierarchy:Changes", "FolderHierarchy:Count"])
|
|
.unwrap()
|
|
.text_value(),
|
|
"0"
|
|
);
|
|
|
|
// ---- Z-Push leftovers: a collection id unknown here must be deleted on the device
|
|
let zr = p
|
|
.sync(
|
|
"a6271970",
|
|
"{11111111-2222-3333-4444-555555555555}7",
|
|
10,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(col(&zr).child_text("AirSync:Status").as_deref(), Some("12"));
|
|
let fs3 = p.folder_sync(&fkey).await.unwrap();
|
|
let ch3 = fs3.child("FolderHierarchy:Changes").expect("changes");
|
|
assert!(
|
|
ch3.children_named("FolderHierarchy:Delete")
|
|
.any(|d| d.child_text("FolderHierarchy:ServerId").as_deref() == Some("a6271970")),
|
|
"stale collection deleted: {}",
|
|
fs3.to_xml()
|
|
);
|
|
let fkey = fs3.child_text("FolderHierarchy:SyncKey").unwrap();
|
|
|
|
// ---- mail
|
|
let (mut ikey, mails) = full_sync(&p, &inbox).await;
|
|
assert!(mails.len() >= 3, "expected mails, got {}", mails.len());
|
|
let mails: Vec<Element> = mails
|
|
.into_iter()
|
|
.filter(|m| {
|
|
m.child("AirSync:ApplicationData")
|
|
.and_then(|a| a.child_text("Email:Subject"))
|
|
.map(|s| s.starts_with("Testowa wiadomo"))
|
|
.unwrap_or(false)
|
|
})
|
|
.collect();
|
|
assert!(mails.len() >= 3, "seeded test mails present");
|
|
let first = &mails[0];
|
|
let app = first.child("AirSync:ApplicationData").unwrap();
|
|
assert!(app
|
|
.child_text("Email:Subject")
|
|
.unwrap()
|
|
.starts_with("Testowa wiadomo"));
|
|
let fref = app
|
|
.path(&["AirSyncBase:Attachments", "AirSyncBase:Attachment"])
|
|
.and_then(|a| a.child_text("AirSyncBase:FileReference"))
|
|
.expect("attachment");
|
|
// attachment via ItemOperations
|
|
let io = Element::new("ItemOperations:ItemOperations").with(
|
|
Element::new("ItemOperations:Fetch")
|
|
.with_text("ItemOperations:Store", "Mailbox")
|
|
.with_text("AirSyncBase:FileReference", fref.clone()),
|
|
);
|
|
let (_, r) = p.cmd("ItemOperations", Some(io)).await.unwrap();
|
|
let r = r.unwrap();
|
|
let data = r
|
|
.path(&[
|
|
"ItemOperations:Response",
|
|
"ItemOperations:Fetch",
|
|
"ItemOperations:Properties",
|
|
])
|
|
.and_then(|x| x.child_text("ItemOperations:Data"))
|
|
.expect("attachment data");
|
|
assert_eq!(data, "JVBERi0xLjQKJcOkw7zDtsOfCg==");
|
|
|
|
// client marks read + flag
|
|
let sid = first.child_text("AirSync:ServerId").unwrap();
|
|
let ch = Element::new("AirSync:Commands").with(
|
|
Element::new("AirSync:Change")
|
|
.with_text("AirSync:ServerId", sid.clone())
|
|
.with(
|
|
Element::new("AirSync:ApplicationData")
|
|
.with_text("Email:Read", "1")
|
|
.with(Element::new("Email:Flag").with_text("Email:FlagStatus", "2")),
|
|
),
|
|
);
|
|
let (k, c) = changes(&p, &inbox, &ikey, Some(ch)).await;
|
|
ikey = k;
|
|
assert!(
|
|
c.child("AirSync:Commands").is_none(),
|
|
"own change must not be echoed: {}",
|
|
c.to_xml()
|
|
);
|
|
let uid: u32 = sid.split(':').nth(1).unwrap().parse().unwrap();
|
|
let mut im = imap().await;
|
|
im.select("INBOX").await.unwrap();
|
|
let fl = im.uid_fetch_flags(&uid.to_string()).await.unwrap();
|
|
assert!(
|
|
fl[0].flags.iter().any(|x| x == "\\Seen") && fl[0].flags.iter().any(|x| x == "\\Flagged")
|
|
);
|
|
|
|
// server side change -> device gets Change
|
|
im.uid_store(uid, "-", "\\Flagged").await.unwrap();
|
|
let (k, c) = changes(&p, &inbox, &ikey, None).await;
|
|
ikey = k;
|
|
let cmds = c.child("AirSync:Commands").expect("server change");
|
|
assert!(cmds.children.iter().any(|x| x.is("AirSync:Change")));
|
|
|
|
// new mail arrives -> Add
|
|
im.append(
|
|
"INBOX",
|
|
"",
|
|
b"From: x@y.pl\r\nTo: jan@test.local\r\nSubject: Nowa\r\n\r\nhej\r\n",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let (k, c) = changes(&p, &inbox, &ikey, None).await;
|
|
ikey = k;
|
|
assert!(c
|
|
.child("AirSync:Commands")
|
|
.unwrap()
|
|
.children
|
|
.iter()
|
|
.any(|x| x.is("AirSync:Add")));
|
|
|
|
// delete from device -> Trash
|
|
let ch = Element::new("AirSync:Commands")
|
|
.with(Element::new("AirSync:Delete").with_text("AirSync:ServerId", sid.clone()));
|
|
let (k, _) = changes(&p, &inbox, &ikey, Some(ch)).await;
|
|
ikey = k;
|
|
im.select("Trash").await.unwrap();
|
|
assert!(
|
|
!im.uid_search("ALL").await.unwrap().is_empty(),
|
|
"message moved to Trash"
|
|
);
|
|
|
|
// GetItemEstimate (nothing pending)
|
|
let gie = Element::new("GetItemEstimate:GetItemEstimate").with(
|
|
Element::new("GetItemEstimate:Collections").with(
|
|
Element::new("GetItemEstimate:Collection")
|
|
.with_text("AirSync:SyncKey", ikey.clone())
|
|
.with_text("GetItemEstimate:CollectionId", inbox.clone()),
|
|
),
|
|
);
|
|
let (_, r) = p.cmd("GetItemEstimate", Some(gie)).await.unwrap();
|
|
let r = r.unwrap();
|
|
let est_dbg = r.to_xml();
|
|
let est = r
|
|
.path(&["GetItemEstimate:Response", "GetItemEstimate:Collection"])
|
|
.unwrap()
|
|
.child_text("GetItemEstimate:Estimate")
|
|
.unwrap();
|
|
if est != "0" {
|
|
let (_, c) = changes(&p, &inbox, &ikey, None).await;
|
|
panic!("estimate {est}: {est_dbg}\npending: {}", c.to_xml());
|
|
}
|
|
|
|
// MoveItems inbox -> Projekty
|
|
let (_, c) = changes(&p, &inbox, &ikey, None).await;
|
|
let _ = c;
|
|
let mv_sid = mails[1].child_text("AirSync:ServerId").unwrap();
|
|
let mv = Element::new("Move:MoveItems").with(
|
|
Element::new("Move:Move")
|
|
.with_text("Move:SrcMsgId", mv_sid)
|
|
.with_text("Move:SrcFldId", inbox.clone())
|
|
.with_text("Move:DstFldId", projekty.clone()),
|
|
);
|
|
let (_, r) = p.cmd("MoveItems", Some(mv)).await.unwrap();
|
|
let r = r.unwrap();
|
|
let resp = r.child("Move:Response").unwrap();
|
|
assert_eq!(
|
|
resp.child_text("Move:Status").as_deref(),
|
|
Some("3"),
|
|
"{}",
|
|
r.to_xml()
|
|
);
|
|
assert!(resp
|
|
.child_text("Move:DstMsgId")
|
|
.unwrap()
|
|
.starts_with(&format!("{projekty}:")));
|
|
|
|
// ---- SendMail + Sent copy
|
|
let mime = format!("From: Jan <{USER}>\r\nTo: anna@test.local\r\nBcc: tajny@test.local\r\nSubject: E2E wysyłka\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nCześć Anno!\r\n");
|
|
let sm = Element::new("ComposeMail:SendMail")
|
|
.with_text("ComposeMail:ClientId", "c1")
|
|
.with_empty("ComposeMail:SaveInSentItems")
|
|
.with(Element::opaque("ComposeMail:MIME", mime.into_bytes()));
|
|
let (st, r) = p.cmd("SendMail", Some(sm)).await.unwrap();
|
|
assert_eq!(st, 200);
|
|
assert!(
|
|
r.is_none(),
|
|
"SendMail success returns empty body: {:?}",
|
|
r.map(|x| x.to_xml())
|
|
);
|
|
im.select("Sent").await.unwrap();
|
|
assert!(
|
|
!im.uid_search("SUBJECT \"E2E\"").await.unwrap().is_empty(),
|
|
"copy in Sent"
|
|
);
|
|
|
|
// SmartReply quoting original
|
|
let reply_src = mails[2].child_text("AirSync:ServerId").unwrap();
|
|
let mime = format!("From: {USER}\r\nTo: anna@test.local\r\nSubject: Re: test\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nOdpowiedź\r\n");
|
|
let sr = Element::new("ComposeMail:SmartReply")
|
|
.with_text("ComposeMail:ClientId", "c2")
|
|
.with(
|
|
Element::new("ComposeMail:Source")
|
|
.with_text("ComposeMail:FolderId", inbox.clone())
|
|
.with_text("ComposeMail:ItemId", reply_src),
|
|
)
|
|
.with(Element::opaque("ComposeMail:MIME", mime.into_bytes()));
|
|
let (st, _) = p.cmd("SmartReply", Some(sr)).await.unwrap();
|
|
assert_eq!(st, 200);
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
let sink = std::fs::read_dir("data/test-backends/sink")
|
|
.map(|d| d.count())
|
|
.unwrap_or(0);
|
|
assert!(sink >= 2, "SMTP sink got messages");
|
|
|
|
// ---- calendar
|
|
let (ckey, evs) = full_sync(&p, &cal).await;
|
|
let ev = evs
|
|
.iter()
|
|
.filter_map(|e| e.child("AirSync:ApplicationData"))
|
|
.find(|a| a.child_text("Calendar:Subject").as_deref() == Some("Spotkanie zespołu"))
|
|
.expect("seeded event");
|
|
assert_eq!(
|
|
ev.child_text("Calendar:Subject").unwrap(),
|
|
"Spotkanie zespołu"
|
|
);
|
|
assert_eq!(
|
|
ev.path(&["Calendar:Recurrence", "Calendar:Type"])
|
|
.unwrap()
|
|
.text_value(),
|
|
"1"
|
|
);
|
|
assert_eq!(ev.child_text("Calendar:Reminder").unwrap(), "15");
|
|
// device adds an event
|
|
let add = Element::new("AirSync:Commands").with(
|
|
Element::new("AirSync:Add")
|
|
.with_text("AirSync:ClientId", "ev-new")
|
|
.with(
|
|
Element::new("AirSync:ApplicationData")
|
|
.with_text(
|
|
"Calendar:TimeZone",
|
|
active_push::backend::tz::blob_for(&chrono_tz::Europe::Warsaw, 2026),
|
|
)
|
|
.with_text("Calendar:AllDayEvent", "0")
|
|
.with_text("Calendar:StartTime", "20261120T090000Z")
|
|
.with_text("Calendar:EndTime", "20261120T100000Z")
|
|
.with_text("Calendar:Subject", "Z telefonu")
|
|
.with_text("Calendar:Reminder", "30")
|
|
.with_text("Calendar:BusyStatus", "2"),
|
|
),
|
|
);
|
|
let (ckey2, c) = changes(&p, &cal, &ckey, Some(add)).await;
|
|
let resp = c.path(&["AirSync:Responses", "AirSync:Add"]).unwrap();
|
|
assert_eq!(resp.child_text("AirSync:Status").as_deref(), Some("1"));
|
|
let new_sid = resp.child_text("AirSync:ServerId").unwrap();
|
|
// idempotent retry with the same key returns the same id
|
|
let add_again = Element::new("AirSync:Commands").with(
|
|
Element::new("AirSync:Add")
|
|
.with_text("AirSync:ClientId", "ev-new")
|
|
.with(Element::new("AirSync:ApplicationData").with_text("Calendar:Subject", "dup")),
|
|
);
|
|
let (_, c2) = changes(&p, &cal, &ckey2, Some(add_again)).await;
|
|
assert_eq!(
|
|
c2.path(&["AirSync:Responses", "AirSync:Add"])
|
|
.unwrap()
|
|
.child_text("AirSync:ServerId")
|
|
.unwrap(),
|
|
new_sid
|
|
);
|
|
// stored in Radicale
|
|
let body = reqwest::Client::new()
|
|
.request(
|
|
reqwest::Method::from_bytes(b"PROPFIND").unwrap(),
|
|
"http://127.0.0.1:15232/jan@test.local/kalendarz/",
|
|
)
|
|
.basic_auth(USER, Some(PASS))
|
|
.header("Depth", "1")
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
body.matches(".ics").count() >= 3,
|
|
"new event stored on CalDAV"
|
|
);
|
|
|
|
// ---- tasks + contacts
|
|
let (_, ts) = full_sync(&p, &tasks).await;
|
|
assert!(ts.iter().any(|t| t
|
|
.child("AirSync:ApplicationData")
|
|
.and_then(|a| a.child_text("Tasks:Subject"))
|
|
.as_deref()
|
|
== Some("Kupić mleko")));
|
|
let (kkey, cs) = full_sync(&p, &contacts).await;
|
|
assert_eq!(cs.len(), 1);
|
|
let c0 = cs[0].child("AirSync:ApplicationData").unwrap();
|
|
assert_eq!(
|
|
c0.child_text("Contacts:MobilePhoneNumber").unwrap(),
|
|
"+48 600 100 200"
|
|
);
|
|
let csid = cs[0].child_text("AirSync:ServerId").unwrap();
|
|
let mut changed = c0.clone();
|
|
changed.children.retain(|x| !x.is("Contacts:JobTitle"));
|
|
changed.push_text("Contacts:JobTitle", "Kierownik");
|
|
let ch = Element::new("AirSync:Commands").with(
|
|
Element::new("AirSync:Change")
|
|
.with_text("AirSync:ServerId", csid)
|
|
.with(changed),
|
|
);
|
|
let (_, c) = changes(&p, &contacts, &kkey, Some(ch)).await;
|
|
assert!(
|
|
c.child("AirSync:Responses").is_none(),
|
|
"contact change ok: {}",
|
|
c.to_xml()
|
|
);
|
|
|
|
// ---- search
|
|
let gal = Element::new("Search:Search").with(
|
|
Element::new("Search:Store")
|
|
.with_text("Search:Name", "GAL")
|
|
.with_text("Search:Query", "Nowak")
|
|
.with(Element::new("Search:Options").with_text("Search:Range", "0-9")),
|
|
);
|
|
let (_, r) = p.cmd("Search", Some(gal)).await.unwrap();
|
|
let r = r.unwrap();
|
|
assert_eq!(
|
|
r.path(&["Search:Response", "Search:Store"])
|
|
.unwrap()
|
|
.child_text("Search:Total")
|
|
.as_deref(),
|
|
Some("1"),
|
|
"{}",
|
|
r.to_xml()
|
|
);
|
|
let ms = Element::new("Search:Search").with(
|
|
Element::new("Search:Store")
|
|
.with_text("Search:Name", "Mailbox")
|
|
.with(
|
|
Element::new("Search:Query").with(
|
|
Element::new("Search:And")
|
|
.with_text("AirSync:Class", "Email")
|
|
.with_text("AirSync:CollectionId", inbox.clone())
|
|
.with_text("Search:FreeText", "wiadomości numer"),
|
|
),
|
|
)
|
|
.with(Element::new("Search:Options").with_text("Search:Range", "0-9")),
|
|
);
|
|
let (_, r) = p.cmd("Search", Some(ms)).await.unwrap();
|
|
let r = r.unwrap();
|
|
let total: u32 = r
|
|
.path(&["Search:Response", "Search:Store"])
|
|
.unwrap()
|
|
.child_text("Search:Total")
|
|
.unwrap()
|
|
.parse()
|
|
.unwrap();
|
|
assert!(total >= 1, "mailbox search: {}", r.to_xml());
|
|
|
|
// ---- folder create / rename / delete
|
|
let fc = Element::new("FolderHierarchy:FolderCreate")
|
|
.with_text("FolderHierarchy:SyncKey", fkey.clone())
|
|
.with_text("FolderHierarchy:ParentId", "0")
|
|
.with_text("FolderHierarchy:DisplayName", "Nowy folder ż")
|
|
.with_text("FolderHierarchy:Type", "12");
|
|
let (_, r) = p.cmd("FolderCreate", Some(fc)).await.unwrap();
|
|
let r = r.unwrap();
|
|
assert_eq!(
|
|
r.child_text("FolderHierarchy:Status").as_deref(),
|
|
Some("1"),
|
|
"{}",
|
|
r.to_xml()
|
|
);
|
|
let nkey = r.child_text("FolderHierarchy:SyncKey").unwrap();
|
|
let nid = r.child_text("FolderHierarchy:ServerId").unwrap();
|
|
let fu = Element::new("FolderHierarchy:FolderUpdate")
|
|
.with_text("FolderHierarchy:SyncKey", nkey)
|
|
.with_text("FolderHierarchy:ServerId", nid.clone())
|
|
.with_text("FolderHierarchy:ParentId", "0")
|
|
.with_text("FolderHierarchy:DisplayName", "Zmieniony");
|
|
let (_, r) = p.cmd("FolderUpdate", Some(fu)).await.unwrap();
|
|
let r = r.unwrap();
|
|
assert_eq!(
|
|
r.child_text("FolderHierarchy:Status").as_deref(),
|
|
Some("1"),
|
|
"{}",
|
|
r.to_xml()
|
|
);
|
|
let fd = Element::new("FolderHierarchy:FolderDelete")
|
|
.with_text(
|
|
"FolderHierarchy:SyncKey",
|
|
r.child_text("FolderHierarchy:SyncKey").unwrap(),
|
|
)
|
|
.with_text("FolderHierarchy:ServerId", nid);
|
|
let (_, r) = p.cmd("FolderDelete", Some(fd)).await.unwrap();
|
|
assert_eq!(
|
|
r.unwrap().child_text("FolderHierarchy:Status").as_deref(),
|
|
Some("1")
|
|
);
|
|
|
|
// ---- settings / user information
|
|
let s = Element::new("Settings:Settings")
|
|
.with(Element::new("Settings:UserInformation").with_empty("Settings:Get"));
|
|
let (_, r) = p.cmd("Settings", Some(s)).await.unwrap();
|
|
assert!(r.unwrap().to_xml().contains(USER));
|
|
|
|
// ---- ping: new mail during heartbeat
|
|
let (fresh_key, _) = changes(&p, &inbox, &ikey, None).await;
|
|
let _ = fresh_key;
|
|
let ping = Element::new("Ping:Ping")
|
|
.with_text("Ping:HeartbeatInterval", "60")
|
|
.with(
|
|
Element::new("Ping:Folders").with(
|
|
Element::new("Ping:Folder")
|
|
.with_text("Ping:Id", inbox.clone())
|
|
.with_text("Ping:Class", "Email"),
|
|
),
|
|
);
|
|
let started = std::time::Instant::now();
|
|
let pinger = tokio::spawn({
|
|
let p2 = Probe::new(&url, USER, PASS, &dev, true).unwrap();
|
|
let key = p.policy_key.clone();
|
|
async move {
|
|
let mut p2 = p2;
|
|
p2.policy_key = key;
|
|
p2.cmd("Ping", Some(ping)).await
|
|
}
|
|
});
|
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
im.append(
|
|
"INBOX",
|
|
"",
|
|
b"From: x@y.pl\r\nTo: jan@test.local\r\nSubject: Push!\r\n\r\npush\r\n",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let (_, r) = pinger.await.unwrap().unwrap();
|
|
let r = r.unwrap();
|
|
assert_eq!(
|
|
r.child_text("Ping:Status").as_deref(),
|
|
Some("2"),
|
|
"{}",
|
|
r.to_xml()
|
|
);
|
|
assert!(
|
|
started.elapsed() < Duration::from_secs(20),
|
|
"push latency {:?}",
|
|
started.elapsed()
|
|
);
|
|
eprintln!("ping reported change after {:?}", started.elapsed());
|
|
|
|
// ---- sent folder sync works too
|
|
let (_, sent_items) = full_sync(&p, &sent).await;
|
|
assert!(!sent_items.is_empty());
|
|
let (_, trash_items) = full_sync(&p, &trash).await;
|
|
assert!(!trash_items.is_empty());
|
|
|
|
// ---- autodiscover
|
|
let base = std::env::var("AP_E2E_URL").unwrap();
|
|
let ad = reqwest::Client::new()
|
|
.post(format!("{base}/autodiscover/autodiscover.xml"))
|
|
.basic_auth(USER, Some(PASS))
|
|
.body(r#"<?xml version="1.0"?><Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006"><Request><EMailAddress>jan@test.local</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006</AcceptableResponseSchema></Request></Autodiscover>"#)
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
ad.contains("<Type>MobileSync</Type>") && ad.contains("Microsoft-Server-ActiveSync"),
|
|
"{ad}"
|
|
);
|
|
eprintln!("e2e OK");
|
|
}
|