use super::*;
use std::cell::{Cell, RefCell};
use std::io::Cursor;
#[derive(Debug, Clone, PartialEq, Eq)]
struct CapturedQuery {
table: String,
key_column: String,
value_column: String,
order_by: String,
}
#[derive(Default)]
struct FakeStore {
rows: RefCell<Vec<SettingRow>>,
last_query: RefCell<Option<CapturedQuery>>,
}
impl FakeStore {
fn with_rows(rows: impl IntoIterator<Item = (&'static str, &'static str)>) -> Self {
Self {
rows: RefCell::new(
rows.into_iter()
.map(|(key, value)| SettingRow {
key: key.to_string(),
value: value.to_string(),
})
.collect(),
),
last_query: RefCell::new(None),
}
}
}
impl SettingsStore for FakeStore {
fn load_settings(
&self,
query: &SettingsQuery<'_>,
) -> Result<Vec<SettingRow>, Box<dyn Error + Send + Sync>> {
*self.last_query.borrow_mut() = Some(CapturedQuery {
table: query.table.to_string(),
key_column: query.key_column.to_string(),
value_column: query.value_column.to_string(),
order_by: query.order_by.to_string(),
});
let mut rows = self.rows.borrow().clone();
if query.order_by == query.key_column {
rows.sort_by(|left, right| left.key.cmp(&right.key));
}
Ok(rows)
}
fn update_setting(
&self,
query: &SettingsQuery<'_>,
key: &str,
value: &str,
) -> Result<(), Box<dyn Error + Send + Sync>> {
*self.last_query.borrow_mut() = Some(CapturedQuery {
table: query.table.to_string(),
key_column: query.key_column.to_string(),
value_column: query.value_column.to_string(),
order_by: query.order_by.to_string(),
});
if let Some(row) = self.rows.borrow_mut().iter_mut().find(|row| row.key == key) {
row.value = value.to_string();
}
Ok(())
}
}
#[test]
fn default_builder_values_load_default_table_ordered_by_key() {
let store = FakeStore::with_rows([("log_level", "info"), ("graph_client_id", "abc123")]);
let menu = ConfigMenu::new(store);
let rows = menu.load_rows().unwrap();
assert_eq!(rows[0].key, "graph_client_id");
assert_eq!(rows[1].key, "log_level");
assert_eq!(
*menu.store.last_query.borrow(),
Some(CapturedQuery {
table: "settings".to_string(),
key_column: "key".to_string(),
value_column: "value".to_string(),
order_by: "key".to_string(),
})
);
}
#[test]
fn masks_non_empty_secret_values_only() {
let menu = ConfigMenu::new(FakeStore::default()).secret_keys(["secret"]);
assert_eq!(
menu.display_value(&SettingRow {
key: "secret".to_string(),
value: "value".to_string(),
}),
"********"
);
assert_eq!(
menu.display_value(&SettingRow {
key: "secret".to_string(),
value: String::new(),
}),
""
);
assert_eq!(
menu.display_value(&SettingRow {
key: "public".to_string(),
value: "value".to_string(),
}),
"value"
);
}
#[test]
fn updates_selected_setting() {
let menu = ConfigMenu::new(FakeStore::with_rows([("log_level", "info")]));
menu.update_row("log_level", "debug").unwrap();
assert_eq!(menu.store.rows.borrow()[0].value, "debug");
}
#[test]
fn validation_failure_prevents_update() {
let menu = ConfigMenu::new(FakeStore::with_rows([("log_level", "info")]))
.validator(|_, _| Err("bad value".into()));
let error = menu.validate_value("log_level", "debug").unwrap_err();
assert!(matches!(error, ConfigEasyError::ValidationFailed { .. }));
assert_eq!(menu.store.rows.borrow()[0].value, "info");
}
#[test]
fn supports_custom_table_and_columns() {
let menu = ConfigMenu::new(FakeStore::with_rows([("log_level", "info")]))
.table("app_config")
.key_column("name")
.value_column("setting")
.order_by("name");
let rows = menu.load_rows().unwrap();
assert_eq!(rows[0].key, "log_level");
assert_eq!(rows[0].value, "info");
assert_eq!(
*menu.store.last_query.borrow(),
Some(CapturedQuery {
table: "app_config".to_string(),
key_column: "name".to_string(),
value_column: "setting".to_string(),
order_by: "name".to_string(),
})
);
}
#[test]
fn supports_custom_ordering_column() {
let menu = ConfigMenu::new(FakeStore::with_rows([("second", "2"), ("first", "1")]))
.order_by("display_order");
menu.load_rows().unwrap();
assert_eq!(
menu.store.last_query.borrow().as_ref().unwrap().order_by,
"display_order"
);
}
#[test]
fn custom_action_callback_runs() {
let called = Cell::new(false);
let menu = ConfigMenu::new(FakeStore::default()).action("r", "reset", || {
called.set(true);
Ok(())
});
menu.run_action(&menu.actions[0]).unwrap();
assert!(called.get());
}
#[test]
fn custom_action_errors_are_wrapped() {
let menu = ConfigMenu::new(FakeStore::default()).action("r", "reset", || Err("failed".into()));
let error = menu.run_action(&menu.actions[0]).unwrap_err();
assert!(matches!(error, ConfigEasyError::ActionFailed { .. }));
}
#[test]
fn run_with_updates_then_exits() {
let menu = ConfigMenu::new(FakeStore::with_rows([("log_level", "info")]));
let input = b"1\ndebug\n\n";
let mut input = Cursor::new(input);
let mut output = Vec::new();
menu.run_with(&mut input, &mut output).unwrap();
assert_eq!(menu.store.rows.borrow()[0].value, "debug");
}
#[test]
fn run_with_clears_screen_before_rendering_menu() {
let menu = ConfigMenu::new(FakeStore::with_rows([("log_level", "info")]));
let input = b"\n";
let mut input = Cursor::new(input);
let mut output = Vec::new();
menu.run_with(&mut input, &mut output).unwrap();
let output = String::from_utf8(output).unwrap();
assert!(output.starts_with("\x1b[2J\x1b[HSettings\n"));
}
#[test]
fn invalid_action_key_fails_before_menu_loop() {
let input = b"\n";
let mut input = Cursor::new(input);
let mut output = Vec::new();
let error = ConfigMenu::new(FakeStore::default())
.action("1", "bad", || Ok(()))
.run_with(&mut input, &mut output)
.unwrap_err();
assert!(matches!(error, ConfigEasyError::InvalidActionKey(_)));
}
#[test]
fn duplicate_action_key_fails_before_menu_loop() {
let input = b"\n";
let mut input = Cursor::new(input);
let mut output = Vec::new();
let error = ConfigMenu::new(FakeStore::default())
.action("r", "reset", || Ok(()))
.action("r", "refresh", || Ok(()))
.run_with(&mut input, &mut output)
.unwrap_err();
assert!(matches!(error, ConfigEasyError::DuplicateActionKey(_)));
}