Something went wrong. Try again.
forked from evanpurkhiser/things3-cloud
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625fn cases(case_glob: &str) { let things_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_things"));
trycmd::TestCases::new() .env("TRYCMD_BIN_THINGS", things_bin.display().to_string()) .register_bin( "run.sh", std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("cli") .join("run.sh"), ) .case(case_glob);}
#[test]fn anytime() { cases("tests/cli/anytime/**/*.trycmd");}
#[test]fn area() { cases("tests/cli/area/**/*.trycmd");}
#[test]fn areas() { cases("tests/cli/areas/**/*.trycmd");}
#[test]fn delete() { cases("tests/cli/delete/**/*.trycmd");}
#[test]fn edit() { cases("tests/cli/edit/**/*.trycmd");}
#[test]fn find() { cases("tests/cli/find/**/*.trycmd");}
#[test]fn inbox() { cases("tests/cli/inbox/**/*.trycmd");}
#[test]fn logbook() { cases("tests/cli/logbook/**/*.trycmd");}
#[test]fn mark() { cases("tests/cli/mark/**/*.trycmd");}
#[test]fn new() { cases("tests/cli/new/**/*.trycmd");}
#[test]fn project() { cases("tests/cli/project/**/*.trycmd");}
#[test]fn projects() { cases("tests/cli/projects/**/*.trycmd");}
#[test]fn reorder() { cases("tests/cli/reorder/**/*.trycmd");}
#[test]fn someday() { cases("tests/cli/someday/**/*.trycmd");}
#[test]fn tags() { cases("tests/cli/tags/**/*.trycmd");}
#[test]fn today() { cases("tests/cli/today/**/*.trycmd");}
#[test]fn trash() { cases("tests/cli/trash/**/*.trycmd");}
#[test]fn upcoming() { cases("tests/cli/upcoming/**/*.trycmd");}
#[test]fn show() { cases("tests/cli/show/**/*.trycmd");}
/// the proxy refuses the connection////// the sync fails without a request leaving the machine/// REQUEST_METHOD is taken out in every such case/// hyper drops each proxy while it is set/// with no journal on disk there is no cached state to show#[test]fn failed_sync() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"user@example.com","password":"hunter2"}"#, ) .expect("auth file"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--json", "inbox"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!( stderr.contains("Sync failed, and no cached state is known to belong to this account"), "{stderr}" ); assert!(output.stdout.is_empty());}
/// a cursor that names the configured email with no journal beside it leaves no cached state to show#[test]fn failed_sync_with_a_cursor_and_no_journal() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); let cache = home.path().join("state").join("things").join("append-log"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::create_dir_all(&cache).expect("cache dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"user@example.com","password":"hunter2"}"#, ) .expect("auth file"); std::fs::write( cache.join("cursor.json"), r#"{"next_start_index":0,"history_key":"k","log_offset":0,"email":"user@example.com"}"#, ) .expect("cursor"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--json", "inbox"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!( stderr.contains("Sync failed, and no cached state is known to belong to this account"), "{stderr}" ); assert!(output.stdout.is_empty());}
/// a journal shorter than the cursor claims is emptied by the repair before the request////// the empty journal that remains is no cached state to show#[test]fn failed_sync_with_a_journal_the_repair_empties() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); let cache = home.path().join("state").join("things").join("append-log"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::create_dir_all(&cache).expect("cache dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"user@example.com","password":"hunter2"}"#, ) .expect("auth file"); std::fs::write( cache.join("things.log"), "{\"Ta11111111111111111111\":{\"t\":0,\"e\":\"Task7\",\"p\":{\"tt\":\"Kept\"}}}\n", ) .expect("journal"); std::fs::write( cache.join("cursor.json"), r#"{"next_start_index":2,"history_key":"k","log_offset":180,"email":"user@example.com"}"#, ) .expect("cursor"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--json", "inbox"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!( stderr.contains("Sync failed, and no cached state is known to belong to this account"), "{stderr}" ); assert!(output.stdout.is_empty());}
/// a cache that another email's sign-in wrote is no fallback when the sync fails#[test]fn failed_sync_with_another_accounts_cache() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); let journal = home.path().join("state").join("things").join("append-log"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::create_dir_all(&journal).expect("journal dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"bob@example.com","password":"hunter2"}"#, ) .expect("auth file"); std::fs::write( journal.join("things.log"), "{\"Ta11111111111111111111\":{\"t\":0,\"e\":\"Task7\",\"p\":{\"tt\":\"Alice private\",\"st\":0,\"ss\":0}}}\n", ) .expect("journal"); std::fs::write( journal.join("cursor.json"), r#"{"next_start_index":1,"history_key":"k","email":"alice@example.com"}"#, ) .expect("cursor"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--json", "inbox"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!( stderr.contains("known to belong to this account"), "{stderr}" ); assert!(!String::from_utf8_lossy(&output.stdout).contains("Alice"));}
/// a cache that cannot be locked fails the run once, not as a failed sync#[test]fn unusable_cache_directory() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"user@example.com","password":"hunter2"}"#, ) .expect("auth file"); let state = home.path().join("state"); std::fs::write(&state, "a file where the state directory goes").expect("state file"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["inbox"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", &state) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!(!stderr.contains("Sync failed"), "{stderr}"); assert_eq!(stderr.lines().count(), 1, "{stderr}");}
/// only a cursor that names the configured email makes its cache a fallback#[test]fn failed_sync_with_a_cache_nobody_vouches_for() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); let journal = home.path().join("state").join("things").join("append-log"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::create_dir_all(&journal).expect("journal dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"bob@example.com","password":"hunter2"}"#, ) .expect("auth file"); std::fs::write( journal.join("things.log"), "{\"Ta11111111111111111111\":{\"t\":0,\"e\":\"Task7\",\"p\":{\"tt\":\"Alice private\",\"st\":0,\"ss\":0}}}\n", ) .expect("journal"); std::fs::write( journal.join("cursor.json"), r#"{"next_start_index":1,"history_key":"k"}"#, ) .expect("cursor"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--json", "inbox"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!(!String::from_utf8_lossy(&output.stdout).contains("Alice"));}
/// a cache that this email's sign-in wrote answers the command when the sync fails#[test]fn failed_sync_with_this_accounts_cache() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); let journal = home.path().join("state").join("things").join("append-log"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::create_dir_all(&journal).expect("journal dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"bob@example.com","password":"hunter2"}"#, ) .expect("auth file"); let line = "{\"Ta11111111111111111111\":{\"t\":0,\"e\":\"Task7\",\"p\":{\"tt\":\"Bob cached\",\"st\":0,\"ss\":0}}}\n"; std::fs::write(journal.join("things.log"), line).expect("journal"); std::fs::write( journal.join("cursor.json"), format!( r#"{{"next_start_index":1,"history_key":"k","log_offset":{},"email":"bob@example.com"}}"#, line.len() ), ) .expect("cursor"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--json", "find", "--any-status"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(3), "{stderr}"); // the stored key went to the items without a sign-in assert!(stderr.contains("the history items from 1"), "{stderr}"); assert!(String::from_utf8_lossy(&output.stdout).contains("Bob cached"));}
/// a cache that cannot be read after a failed sync names both failures on one line#[test]fn failed_sync_with_a_cache_that_does_not_read() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); let journal = home.path().join("state").join("things").join("append-log"); std::fs::create_dir_all(config.join("things")).expect("config dir"); std::fs::create_dir_all(&journal).expect("journal dir"); std::fs::write( config.join("things").join("auth.json"), r#"{"email":"bob@example.com","password":"hunter2"}"#, ) .expect("auth file"); std::fs::write(journal.join("things.log"), "{not json}\n").expect("journal"); std::fs::write( journal.join("cursor.json"), r#"{"next_start_index":1,"history_key":"k","log_offset":11,"email":"bob@example.com"}"#, ) .expect("cursor"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["inbox"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env("HTTPS_PROXY", "http://127.0.0.1:1") .env_remove("NO_PROXY") .env_remove("no_proxy") .env_remove("REQUEST_METHOD") .env_remove("THINGS_EMAIL") .env_remove("THINGS_PASSWORD") .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!(stderr.contains("Sync failed ("), "{stderr}"); assert!(stderr.contains("the sync cache cannot be read"), "{stderr}"); assert!(stderr.contains("Corrupt journal line"), "{stderr}"); assert_eq!(stderr.lines().count(), 1, "{stderr}");}
/// `auth` names a missing place for the auth file before it asks for anything#[test]fn auth_without_a_place_for_the_auth_file() { let home = tempfile::tempdir().expect("tempdir"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--no-cloud", "auth"]) .current_dir(home.path()) .env("HOME", "home") .env("XDG_CONFIG_HOME", "config") .env("XDG_STATE_HOME", home.path().join("state")) .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .stdin(std::process::Stdio::null()) .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!( stderr.contains("No absolute home directory, set XDG_CONFIG_HOME."), "{stderr}" ); assert!(output.stdout.is_empty());}
/// a config home that cannot hold the auth file fails before anything is asked#[test]fn auth_with_a_config_home_that_is_a_file() { let home = tempfile::tempdir().expect("tempdir"); let config = home.path().join("config"); std::fs::write(&config, "").expect("a file where the config home goes"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--no-cloud", "auth"]) .env("XDG_CONFIG_HOME", &config) .env("XDG_STATE_HOME", home.path().join("state")) .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .stdin(std::process::Stdio::null()) .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!(output.status.code(), Some(1), "{stderr}"); assert!( stderr.contains("Failed to save the auth file at"), "{stderr}" ); assert!(output.stdout.is_empty());}
/// a reader that closed stderr leaves the exit status as it is#[test]fn closed_stderr() { let home = tempfile::tempdir().expect("tempdir"); let (reader, writer) = std::io::pipe().expect("pipe"); drop(reader); let status = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--no-cloud", "--load-journal", "missing.json", "today"]) .current_dir(home.path()) .env("XDG_CONFIG_HOME", home.path().join("config")) .env("XDG_STATE_HOME", home.path().join("state")) .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .stdout(std::process::Stdio::null()) .stderr(writer) .status() .expect("things runs"); assert_eq!(status.code(), Some(1));}
/// a log directive that does not parse is reported without touching the exit status#[test]fn bad_log_directive_with_closed_stderr() { let home = tempfile::tempdir().expect("tempdir"); let journal = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("cli") .join("today") .join("basic_list.in") .join("journal.json"); let (reader, writer) = std::io::pipe().expect("pipe"); drop(reader); let status = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--no-cloud", "--load-journal"]) .arg(&journal) .args(["--today-ts", "1774396800", "today"]) .env("XDG_CONFIG_HOME", home.path().join("config")) .env("XDG_STATE_HOME", home.path().join("state")) .env("THINGS_LOG", "things=nope") .env_remove("THINGS_LOG_FORMAT") .stdout(std::process::Stdio::null()) .stderr(writer) .status() .expect("things runs"); assert_eq!(status.code(), Some(0));}
/// a log setting that cannot apply is reported////// the command runs on with the default in its place#[cfg(unix)]#[test]fn unread_log_settings_are_reported() { use std::os::unix::ffi::OsStringExt; let home = tempfile::tempdir().expect("tempdir"); let journal = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("cli") .join("today") .join("basic_list.in") .join("journal.json"); let run = |name: &str, value: std::ffi::OsString| { let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--no-cloud", "--load-journal"]) .arg(&journal) .args(["--today-ts", "1774396800", "today"]) .env("XDG_CONFIG_HOME", home.path().join("config")) .env("XDG_STATE_HOME", home.path().join("state")) .env_remove("THINGS_LOG") .env_remove("THINGS_LOG_FORMAT") .env(name, value) .output() .expect("things runs"); assert_eq!(output.status.code(), Some(0)); String::from_utf8_lossy(&output.stderr).into_owned() }; assert_eq!( run("THINGS_LOG_FORMAT", "JSON".into()), "THINGS_LOG_FORMAT is ignored: \"JSON\" is not pretty, simplified or json\n" ); assert_eq!( run("THINGS_LOG", std::ffi::OsString::from_vec(vec![0xff])), "THINGS_LOG is ignored: \"\\xFF\" is not valid UTF-8\n" ); let unparsed = run("THINGS_LOG", "things=nope".into()); assert!( unparsed.starts_with("THINGS_LOG is ignored: "), "{unparsed}" ); assert_eq!(unparsed.lines().count(), 1, "{unparsed}");}
/// log fields carry cloud text escaped////// the terminal output carries it escaped too#[test]fn logs_escape_cloud_text() { let home = tempfile::tempdir().expect("tempdir"); let journal = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("cli") .join("logs") .join("hostile_journal.json"); let output = std::process::Command::new(env!("CARGO_BIN_EXE_things")) .args(["--no-cloud", "--load-journal"]) .arg(&journal) .args([ "--id-seed", "7", "--today-ts", "1774396800", "--now-ts", "1774400000.0", "today", ]) .env("XDG_CONFIG_HOME", home.path().join("config")) .env("XDG_STATE_HOME", home.path().join("state")) .env("THINGS_LOG", "debug") .env_remove("THINGS_LOG_FORMAT") .output() .expect("things runs"); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("not base58"), "{stderr}"); assert!(stderr.contains("cloud commit request"), "{stderr}"); let raw = stderr .chars() .filter(|&c| { (c.is_control() && c != '\n' && c != '\t') || ('\u{202a}'..='\u{202e}').contains(&c) }) .collect::<String>(); assert!(raw.is_empty(), "{raw:?} in {stderr}");}