//! Accounts in the window: add, check, keep, remove (C12.5). use std::{ collections::HashMap, path::Path, sync::{ Arc, Mutex, atomic::{AtomicUsize, Ordering}, }, time::{Duration, Instant}, }; use ely_mail_engine::{ Account, Auth, Backend, Checked, Protocol, Security, Server, Store, TlsConnector, Vault, }; use gpui::{App, Entity, TestAppContext, VisualTestContext}; use crate::{ adding::Step, kinds::Kind, mail::{Mail, Parts}, }; mod controls; mod flows; mod syncs; /// A credential store that keeps or refuses. #[derive(Clone, Default)] pub(crate) struct Keys { pub kept: Arc>>, pub refuse: Option<&'static str>, pub deletes: Arc, } impl Backend for Keys { fn get(&self, name: &str) -> Result, String> { Ok(self.kept.lock().unwrap().get(name).cloned()) } fn set(&self, name: &str, secret: &str) -> Result<(), String> { if let Some(reason) = self.refuse { return Err(reason.into()); } self.kept.lock().unwrap().insert(name.into(), secret.into()); Ok(()) } fn delete(&self, name: &str) -> Result<(), String> { self.deletes.fetch_add(1, Ordering::SeqCst); if let Some(reason) = self.refuse { return Err(reason.into()); } self.kept.lock().unwrap().remove(name); Ok(()) } } pub(crate) fn settle(cx: &mut VisualTestContext, done: impl Fn(&App) -> bool) { let deadline = Instant::now() + Duration::from_secs(20); while Instant::now() < deadline { std::thread::sleep(Duration::from_millis(20)); cx.run_until_parked(); cx.update(|window, _| window.refresh()); if cx.update(|_, app| done(app)) { return; } } panic!("the state never settled"); } pub(crate) fn window<'a>( cx: &'a mut TestAppContext, dir: &Path, keys: Keys, ) -> (Entity, &'a mut VisualTestContext) { let tls = ely_mail_engine::system_tls().map_err(|err| err.to_string()); window_with(cx, dir, keys, tls) } pub(crate) fn window_with<'a>( cx: &'a mut TestAppContext, dir: &Path, keys: Keys, tls: Result, ) -> (Entity, &'a mut VisualTestContext) { cx.executor().allow_parking(); cx.update(ely_gpui_component::init_for_tests); cx.update(|cx| { ely_ui::settings::install_defaults(cx); ely_ui::i18n::install_as("en-US", cx); // As tool_root does: menus before the window. let menus = crate::menu::menus(cx); let menus = ely_ui::commands::menus( ely_platform::AppId::Mail, &ely_ui::commands::OPENING, false, menus, cx, ); cx.set_menus(menus); }); let parts = Parts { store: Store::open(dir).map_err(|err| err.to_string()), vault: Vault::new(Box::new(keys)), tls, }; let (mail, cx) = cx.add_window_view(move |_, cx| Mail::new(parts, cx)); settle(cx, |app| mail.read(app).accounts.is_some()); (mail, cx) } /// A port nothing listens on. pub(crate) fn closed_port() -> u16 { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); listener.local_addr().unwrap().port() } /// On this machine's discard port, so syncs end at once. pub(crate) fn account(address: &str) -> Account { let server = |port| Server { host: "127.0.0.1".into(), port, security: Security::Tls, }; Account { address: address.into(), user: address.into(), auth: Auth::Password, protocol: Protocol::Imap, incoming: server(9), outgoing: server(9), } } /// Opens the dialog on `kind`. pub(crate) fn adding(mail: &Entity, kind: Kind, cx: &mut VisualTestContext) { cx.update(|window, app| { mail.update(app, |mail, cx| { mail.start_adding(window, cx); mail.adding.as_mut().unwrap().set_kind(kind, cx); }) }); } pub(crate) fn type_in( mail: &Entity, cx: &mut VisualTestContext, pick: fn(&crate::adding::Fields) -> &Entity, words: &str, ) { cx.update(|_, app| { let field = pick(&mail.read(app).adding.as_ref().unwrap().fields).clone(); field.update(app, |field, cx| field.set_text(words.to_owned(), cx)); }); cx.run_until_parked(); } /// A passed check, as servers would answer. pub(crate) fn passed( mail: &Entity, account: Account, kept: &str, cx: &mut VisualTestContext, ) { cx.update(|_, app| { mail.update(app, |mail, _| { mail.adding.as_mut().unwrap().step = Step::Checked { account, kept: kept.into(), checked: Checked { incoming: Ok(()), outgoing: Ok(()), }, }; }) }); } pub(crate) fn step_words(mail: &Entity, cx: &mut VisualTestContext) -> String { cx.update( |_, app| match &mail.read(app).adding.as_ref().unwrap().step { Step::Failed(words) => words.to_string(), Step::Editing => "editing".into(), Step::Signing => "signing".into(), Step::Checking => "checking".into(), Step::Saving => "saving".into(), Step::Checked { .. } => "checked".into(), }, ) }