Files
james.maes 3508b5e10a refactor(cli): drop the runner's dead ephemeral store
The headless runner still built an ephemeral DatabaseEngine, an EventBus,
and a TempDir that no command read -- a remnant of the CLI-holds-its-own-store
design that predates the gRPC API. Nothing but two dead_code accessors touched
them, so remove the fields, the accessors, and the now-unreachable RunnerError.

With no store to open, construction is neither fallible nor async, so the
constructors lose their Result and async, and 'ephemeral' no longer describes
them: they are renamed connect/connect_with/connect_with_token_override.

Doc comments that contrasted the daemon's persistent store against 'this
runner's own ephemeral local db' now described a field that does not exist;
they are rewritten to say the CLI keeps no store at all.
2026-08-14 20:28:27 -05:00

259 lines
9.0 KiB
Rust

//! E2E System Test Suite for nuncio-cli.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use nuncio_cli::{
AccountSubcommand, CalSubcommand, Commands, FolderSubcommand, HeadlessRunner, MailSubcommand,
PasswordArg, SystemSubcommand,
};
use nuncio_store::vault::SecretManager;
use serde_json::Value;
use std::sync::Arc;
#[tokio::test]
async fn system_test_cli_noun_verb_execution_matrix() {
let runner = HeadlessRunner::connect();
// 1. System status is a real gRPC client of the `nunciod` daemon, so it
// is exercised separately below via `ephemeral_with` + `SecretManager::mock()`
// rather than through this `ephemeral()`-constructed runner (which holds
// the production `SecretManager` and must never touch the real OS
// keyring in a test).
// 1b. Banner output
let out: String = runner.execute_command(&Commands::Banner, true).await;
let json: Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(json["status"], "ok");
assert_eq!(json["data"]["name"], "Nuncio");
// 1c. Licenses output
let out: String = runner.execute_command(&Commands::Licenses, true).await;
let json: Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(json["status"], "ok");
// 2. Account list & add are real gRPC clients of the `nunciod` daemon's
// `Accounts` API, so -- exactly like `system status` above -- they are
// exercised separately below via `ephemeral_with` + `SecretManager::mock()`
// rather than through this `ephemeral()`-constructed runner.
// 3. Folder list & 4. Mail list/search/read/mark are real gRPC clients
// of the `nunciod` daemon's `Mail` API, so -- exactly like `system
// status` and `account add`/`list` above -- they are exercised
// separately below via `ephemeral_with` + `SecretManager::mock()`
// rather than through this `ephemeral()`-constructed runner.
// 5. Calendar list & sync are real gRPC clients of the `nunciod`
// daemon's `Calendar` API, so -- exactly like Folder/Mail above -- they
// are exercised separately below via `ephemeral_with` +
// `SecretManager::mock()` rather than through this
// `ephemeral()`-constructed runner.
}
/// `system status` is a real gRPC client of the `nunciod` daemon. With no
/// daemon reachable at an address nothing is listening on, it must report
/// a clear, honest error rather than fabricating a status.
#[tokio::test]
async fn system_status_reports_honest_error_when_daemon_unreachable() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral loopback port");
let addr = listener.local_addr().expect("listener has local addr");
drop(listener); // free the port; nothing is listening on it now
let runner = HeadlessRunner::connect_with(Arc::new(SecretManager::mock()), addr.to_string());
let out: String = runner
.execute_command(
&Commands::System {
action: SystemSubcommand::Status,
},
true,
)
.await;
let json: Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(json["status"], "error");
assert!(json["error"]
.as_str()
.expect("error message present")
.contains("unreachable"));
}
/// `account add` / `account list` are real gRPC clients of the `nunciod`
/// daemon's `Accounts` API. With no daemon reachable, both must report a
/// clear, honest error rather than fabricating success or silently
/// falling back to any local state.
#[tokio::test]
async fn account_add_and_list_report_honest_errors_when_daemon_unreachable() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral loopback port");
let addr = listener.local_addr().expect("listener has local addr");
drop(listener); // free the port; nothing is listening on it now
let runner = HeadlessRunner::connect_with(Arc::new(SecretManager::mock()), addr.to_string());
let list_out: String = runner
.execute_command(
&Commands::Account {
action: AccountSubcommand::List,
},
true,
)
.await;
let list_json: Value = serde_json::from_str(&list_out).expect("valid json");
assert_eq!(list_json["status"], "error");
assert!(list_json["error"]
.as_str()
.expect("error message present")
.contains("unreachable"));
let add_out: String = runner
.execute_command(
&Commands::Account {
action: AccountSubcommand::Add {
email: "test@nuncio.mx".to_string(),
protocol: "imap-smtp".to_string(),
collection_url: None,
imap_host: "mail.nuncio.mx".to_string(),
imap_port: 993,
smtp_host: "mail.nuncio.mx".to_string(),
smtp_port: 465,
imap_mode: "implicit_tls".to_string(),
smtp_mode: "implicit_tls".to_string(),
password: PasswordArg("irrelevant-unreachable-daemon".to_string()),
},
},
true,
)
.await;
let add_json: Value = serde_json::from_str(&add_out).expect("valid json");
assert_eq!(add_json["status"], "error");
assert!(add_json["error"]
.as_str()
.expect("error message present")
.contains("unreachable"));
// The password must never leak into an error message either.
assert!(!add_out.contains("irrelevant-unreachable-daemon"));
}
/// `folder list`, `mail list`, `mail read`, `mail search`, `mail mark`, and
/// `cal list`/`cal sync` are real gRPC clients of the `nunciod` daemon's
/// `Mail`/`Calendar` APIs. With no daemon reachable, all of them must report
/// a clear, honest error rather than fabricating an empty result.
#[tokio::test]
async fn mail_and_folder_report_honest_errors_when_daemon_unreachable() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral loopback port");
let addr = listener.local_addr().expect("listener has local addr");
drop(listener); // free the port; nothing is listening on it now
let runner = HeadlessRunner::connect_with(Arc::new(SecretManager::mock()), addr.to_string());
let assert_honest_error = |out: String| {
let json: Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(json["status"], "error");
assert!(json["error"]
.as_str()
.expect("error message present")
.contains("unreachable"));
};
assert_honest_error(
runner
.execute_command(
&Commands::Folder {
action: FolderSubcommand::List,
},
true,
)
.await,
);
assert_honest_error(
runner
.execute_command(
&Commands::Mail {
action: MailSubcommand::List {
folder: "INBOX".to_string(),
},
},
true,
)
.await,
);
assert_honest_error(
runner
.execute_command(
&Commands::Mail {
action: MailSubcommand::Read {
id: "msg-1".to_string(),
},
},
true,
)
.await,
);
assert_honest_error(
runner
.execute_command(
&Commands::Mail {
action: MailSubcommand::Search {
query: "Architecture".to_string(),
},
},
true,
)
.await,
);
assert_honest_error(
runner
.execute_command(
&Commands::Mail {
action: MailSubcommand::Mark {
id: "msg-1".to_string(),
read: true,
unread: false,
},
},
true,
)
.await,
);
assert_honest_error(
runner
.execute_command(
&Commands::Cal {
action: CalSubcommand::List {
account: "acct-1".to_string(),
calendar: "default".to_string(),
start: 0,
end: i64::MAX,
},
},
true,
)
.await,
);
assert_honest_error(
runner
.execute_command(
&Commands::Cal {
action: CalSubcommand::Sync {
account: "acct-1".to_string(),
calendar: "default".to_string(),
start: 0,
end: i64::MAX,
},
},
true,
)
.await,
);
}