diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74f8790..ba97969 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,14 @@ jobs: toolchain: stable override: true + - name: Install Geckodriver + uses: browser-actions/setup-geckodriver@latest + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Start Geckodriver + run: geckodriver & + - name: Cache uses: Swatinem/rust-cache@v2 @@ -57,6 +65,17 @@ jobs: with: command: test args: --all-features --workspace + env: + TUTANOTA_CLI_USERNAME: ${{ secrets.TUTANOTA_CLI_USERNAME }} + TUTANOTA_CLI_PASSWORD: ${{ secrets.TUTANOTA_CLI_PASSWORD }} + + - name: Preserve Screenshots + uses: actions/upload-artifact@v3 + if: failure() + with: + name: screenshots + path: "*.png" + if-no-files-found: ignore - name: cargo doc uses: actions-rs/cargo@v1 diff --git a/Cargo.lock b/Cargo.lock index c665e92..7917a9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,21 @@ version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +[[package]] +name = "assert_cmd" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.72" @@ -140,6 +155,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "regex-automata 0.3.6", + "serde", +] + [[package]] name = "bumpalo" version = "3.11.1" @@ -308,12 +334,30 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "errno" version = "0.2.8" @@ -632,6 +676,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.5" @@ -699,7 +752,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -841,6 +894,34 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "predicates" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +dependencies = [ + "anstyle", + "difflib", + "itertools", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -912,6 +993,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" + [[package]] name = "regex-syntax" version = "0.6.28" @@ -1181,10 +1268,12 @@ name = "tatutanatata" version = "0.1.0" dependencies = [ "anyhow", + "assert_cmd", "async-trait", "clap", "dotenvy", "futures", + "predicates", "thirtyfour", "tokio", "tracing", @@ -1201,6 +1290,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thirtyfour" version = "0.31.0" @@ -1507,6 +1602,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index b9a5a96..6fe07c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,7 @@ tokio = { version = "1.30.0", features = ["macros", "rt-multi-thread"] } tracing = "0.1.38" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } + +[dev-dependencies] +assert_cmd = "2.0.12" +predicates = { version = "3.0.3", default-features = false } diff --git a/src/commands/export.rs b/src/commands/export.rs index 68d6f72..a36dab7 100644 --- a/src/commands/export.rs +++ b/src/commands/export.rs @@ -197,6 +197,12 @@ async fn is_mail_list_loading(webdriver: &WebDriver) -> Result { async fn navigate_to_folder(folder: &str, webdriver: &WebDriver) -> Result<()> { for (anchor, title) in list_folders(webdriver).await.context("list folders")? { if title == folder { + // modal might be left-over from some login dialog, make sure it is gone before we + // attempt to click any buttons + ensure_modal_is_closed(webdriver) + .await + .context("ensure modal is closed")?; + anchor.click().await.context("clicking folder link")?; ensure_list_is_ready(webdriver) diff --git a/src/logging.rs b/src/logging.rs index 15a4e7c..5cd6a42 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -20,13 +20,13 @@ pub struct LoggingCLIConfig { pub fn setup_logging(config: LoggingCLIConfig) -> Result<()> { LogTracer::init()?; - let filter = match config.log_verbose_count { + let base_filter = match config.log_verbose_count { 0 => "warn", 1 => "info", 2 => "debug", _ => "trace", }; - let filter = EnvFilter::try_new(filter)?; + let filter = EnvFilter::try_new(format!("{base_filter},hyper=info"))?; let subscriber = FmtSubscriber::builder().with_env_filter(filter).finish(); diff --git a/src/login.rs b/src/login.rs index 36d69ff..9957a19 100644 --- a/src/login.rs +++ b/src/login.rs @@ -72,6 +72,10 @@ pub async fn perform_login(config: LoginCLIConfig, webdriver: &WebDriver) -> Res .context("wait for login")??; debug!("login done"); + confirm_dialog(webdriver) + .await + .context("confirm potential dialog")?; + Ok(()) } @@ -94,3 +98,24 @@ async fn has_new_email_button(webdriver: &WebDriver) -> Result { Ok(false) } + +async fn confirm_dialog(webdriver: &WebDriver) -> Result<()> { + debug!("confirm potential dialogs"); + + let Some(dialog) = webdriver.find_at_most_one(By::ClassName("dialog")).await.context("find dialog box")? else { + debug!("no dialog found"); + return Ok(()); + }; + debug!("found dialog, trying to click OK"); + + let ok_button = dialog + .find_one_with_attr(By::Tag("button"), "title", "Ok") + .await + .context("find OK button")?; + debug!("found OK button"); + + ok_button.click().await.context("click OK button")?; + debug!("clicked OK button"); + + Ok(()) +} diff --git a/src/webdriver.rs b/src/webdriver.rs index c2f013f..976a679 100644 --- a/src/webdriver.rs +++ b/src/webdriver.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use clap::Parser; @@ -13,6 +13,14 @@ use crate::error::MultiResultExt; /// Webdriver CLI config. #[derive(Debug, Parser)] pub struct WebdriverCLIConfig { + /// Create screenshot on failure. + #[clap(long, default_value_t = false)] + screenshot_on_failure: bool, + + /// Path for screenshot. + #[clap(long, default_value = "./screenshot.png")] + screenshot_path: PathBuf, + /// Webdriver port. #[clap(long, default_value_t = 4444)] webdriver_port: u16, @@ -50,15 +58,25 @@ where let driver = WebDriver::new(&addr, caps) .await .context("webdriver setup")?; + driver.maximize_window().await.context("maximize window")?; debug!("webdriver setup done"); - let res = f(&driver).await; + let res_f = f(&driver).await; - driver - .quit() - .await - .context("webdriver shutdown") - .combine(res) + let res_screenshot = if res_f.is_err() && config.screenshot_on_failure { + driver + .screenshot(&config.screenshot_path) + .await + .context("create screenshot") + } else { + Ok(()) + }; + + let res_shutdown = driver.quit().await.context("webdriver shutdown"); + + res_shutdown + .combine(res_screenshot) + .combine(res_f) .map_err(|e| e.into_anyhow())?; debug!("webdriver shutdown done"); diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..a8360da --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,53 @@ +use std::sync::{LockResult, Mutex, MutexGuard}; + +use assert_cmd::Command; +use predicates::prelude::*; + +/// We can only have a single webdriver session. +static WEBDRIVER_MUTEX: Mutex<()> = Mutex::new(()); + +#[test] +fn test_help() { + let mut cmd = cmd(); + cmd.arg("--help").assert().success(); +} + +#[test] +fn test_list_folders() { + let _guard = webdriver_mutex(); + let mut cmd = cmd(); + cmd.arg("--screenshot-on-failure") + .arg("--screenshot-path=test_list_folders.png") + .arg("-vv") + .arg("list-folders") + .assert() + .success() + .stdout(predicate::str::contains( + ["Inbox", "Drafts", "Sent", "Trash", "Archive", "Spam"].join("\n"), + )); +} + +#[test] +fn test_export() { + let _guard = webdriver_mutex(); + let mut cmd = cmd(); + cmd.arg("--screenshot-on-failure") + .arg("--screenshot-path=test_export.png") + .arg("-vv") + .arg("export") + .arg("--folder=Archive") + .assert() + .success(); +} + +fn cmd() -> Command { + Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap() +} + +fn webdriver_mutex() -> MutexGuard<'static, ()> { + match WEBDRIVER_MUTEX.lock() { + LockResult::Ok(guard) => guard, + // poisoned locks are OK + LockResult::Err(e) => e.into_inner(), + } +}