use super::*;
#[derive(Default)]
struct Fake {
saved: Vec<ConfigFile>,
fail_save: Option<String>,
checks: Vec<Profile>,
check_result: Option<Result<String, String>>,
}
impl Services for Fake {
fn save(&mut self, file: &ConfigFile) -> Result<(), String> {
if let Some(e) = &self.fail_save {
return Err(e.clone());
}
self.saved.push(file.clone());
Ok(())
}
fn check(&mut self, profile: &Profile) -> Result<String, String> {
self.checks.push(profile.clone());
self.check_result
.clone()
.unwrap_or_else(|| Ok("ok".to_owned()))
}
}
fn profile(pairs: &[(&str, &str)]) -> Profile {
let mut p = Profile::default();
for (k, v) in pairs {
p.set(k, v).unwrap();
}
p
}
fn file() -> ConfigFile {
let mut f = ConfigFile::default();
f.add("local", profile(&[("url", "http://127.0.0.1:8080")]))
.unwrap();
f.add(
"staging",
profile(&[("url", "https://taba-stg:8080"), ("token-file", "/sec/tok")]),
)
.unwrap();
f.add("production", Profile::default()).unwrap();
f
}
fn app() -> App {
App::new(file(), "C:\\cfg\\taba\\config.yml".into(), None)
}
fn press(app: &mut App, fake: &mut Fake, keys: &[Key]) -> Flow {
let mut flow = Flow::Continue;
for k in keys {
flow = app.handle(*k, fake);
}
flow
}
fn typed(text: &str) -> Vec<Key> {
text.chars().map(Key::Char).collect()
}
fn shown(app: &App) -> String {
app.render(Style { color: false }, 100, 30).lines.join("\n")
}
#[test]
fn the_list_shows_profiles_with_the_default_and_the_session_profile_and_never_the_token() {
let mut f = file();
f.get_mut("staging")
.unwrap()
.set("token", "tb_secreto")
.unwrap();
let app = App::new(f, "C:\\cfg\\config.yml".into(), Some("staging".into()));
let text = shown(&app);
assert!(
text.contains("taba-cli profiles") && text.contains("C:\\cfg\\config.yml"),
"{text}"
);
assert!(text.contains("▶ local") && text.contains("●"), "{text}");
assert!(
text.contains("staging (session)") && text.contains("○"),
"{text}"
);
assert!(
text.contains("/sec/tok") && !text.contains("tb_secreto"),
"{text}"
);
assert!(text.contains("↑↓ choose") && text.contains("Del remove"));
}
#[test]
fn up_and_down_move_the_selection_and_stay_inside_the_list() {
let mut a = app();
let mut fake = Fake::default();
press(
&mut a,
&mut fake,
&[Key::Down, Key::Down, Key::Down, Key::Down],
);
assert_eq!(a.selected, 2);
press(&mut a, &mut fake, &[Key::Up, Key::Up, Key::Up]);
assert_eq!(a.selected, 0);
assert!(shown(&a).contains("▶ local"));
}
#[test]
fn an_empty_list_says_so_and_offers_n() {
let a = App::new(ConfigFile::default(), "p".into(), None);
let text = shown(&a);
assert!(
text.contains("There are no profiles") && text.contains("n to create"),
"{text}"
);
}
#[test]
fn q_escape_and_ctrl_c_leave_the_screen() {
for key in [Key::Char('q'), Key::Esc, Key::Ctrl('c'), Key::Ctrl('d')] {
let mut a = app();
assert_eq!(a.handle(key, &mut Fake::default()), Flow::Quit, "{key:?}");
}
}
#[test]
fn d_makes_the_selected_profile_the_default_and_saves() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Down, Key::Char('d')]);
assert_eq!(fake.saved.len(), 1);
assert_eq!(fake.saved[0].default.as_deref(), Some("staging"));
assert!(shown(&a).contains("`staging` is the default profile."));
}
#[test]
fn removing_asks_first_and_only_s_removes() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Down, Key::Delete]);
assert!(shown(&a).contains("Remove \"staging\"? [y/N]"));
press(&mut a, &mut fake, &[Key::Enter]);
assert!(fake.saved.is_empty());
assert_eq!(a.file().names(), ["local", "staging", "production"]);
assert!(shown(&a).contains("Nothing was removed."));
press(&mut a, &mut fake, &[Key::Char('r'), Key::Char('y')]);
assert_eq!(fake.saved.last().unwrap().names(), ["local", "production"]);
assert_eq!(a.file().names(), ["local", "production"]);
}
#[test]
fn removing_the_default_leaves_the_file_without_one() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Char('r'), Key::Char('y')]);
assert_eq!(fake.saved[0].default, None);
}
#[test]
fn a_new_profile_is_created_through_the_form_and_the_first_one_becomes_the_default() {
let mut a = App::new(ConfigFile::default(), "p".into(), None);
let mut fake = Fake::default();
let mut keys = vec![Key::Char('n')];
keys.extend(typed("local"));
keys.push(Key::Down);
keys.extend(typed("http://10.0.0.1:8080"));
keys.push(Key::Ctrl('s'));
press(&mut a, &mut fake, &keys);
let saved = fake.saved.last().unwrap();
assert_eq!(saved.default.as_deref(), Some("local"));
assert_eq!(
saved.get("local").unwrap().get("url"),
Some("http://10.0.0.1:8080")
);
assert!(shown(&a).contains("Profile `local` saved."));
assert_eq!(a.file(), saved);
}
#[test]
fn a_second_profile_does_not_take_the_default_unless_the_box_is_ticked() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Char('n')]);
press(&mut a, &mut fake, &typed("new"));
press(&mut a, &mut fake, &[Key::Ctrl('s')]);
assert_eq!(fake.saved.last().unwrap().default.as_deref(), Some("local"));
press(&mut a, &mut fake, &[Key::Char('n')]);
press(&mut a, &mut fake, &typed("other"));
press(&mut a, &mut fake, &[Key::Down; 10]);
press(&mut a, &mut fake, &[Key::Char(' '), Key::Ctrl('s')]);
assert_eq!(fake.saved.last().unwrap().default.as_deref(), Some("other"));
}
#[test]
fn save_is_refused_while_a_field_is_invalid() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Char('n')]);
press(&mut a, &mut fake, &typed("x"));
press(&mut a, &mut fake, &[Key::Down]);
press(&mut a, &mut fake, &typed("no-scheme"));
press(&mut a, &mut fake, &[Key::Ctrl('s')]);
assert!(fake.saved.is_empty());
let text = shown(&a);
assert!(
text.contains("Fix the fields in red before saving."),
"{text}"
);
assert!(text.contains("http://"), "{text}");
}
#[test]
fn editing_an_existing_profile_changes_it_in_place_and_a_rename_keeps_the_default() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Enter]);
assert!(shown(&a).contains("Profile local"));
press(&mut a, &mut fake, &[Key::Ctrl('u')]);
press(&mut a, &mut fake, &typed("principal"));
press(&mut a, &mut fake, &[Key::Ctrl('s')]);
let saved = fake.saved.last().unwrap();
assert_eq!(saved.names(), ["principal", "staging", "production"]);
assert_eq!(saved.default.as_deref(), Some("principal"));
assert_eq!(
saved.get("principal").unwrap().get("url"),
Some("http://127.0.0.1:8080")
);
}
#[test]
fn unticking_the_box_of_the_default_profile_removes_the_default() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Enter]);
press(&mut a, &mut fake, &[Key::Down; 10]);
press(&mut a, &mut fake, &[Key::Char(' '), Key::Ctrl('s')]);
assert_eq!(fake.saved.last().unwrap().default, None);
}
#[test]
fn test_connection_uses_the_unsaved_values_and_only_reports() {
let mut a = app();
let mut fake = Fake {
check_result: Some(Ok("ok: http://x:1 answered in 12 ms".to_owned())),
..Fake::default()
};
press(&mut a, &mut fake, &[Key::Char('n')]);
press(&mut a, &mut fake, &typed("t"));
press(&mut a, &mut fake, &[Key::Down]);
press(&mut a, &mut fake, &typed("http://x:1"));
press(&mut a, &mut fake, &[Key::Down; 11]);
press(&mut a, &mut fake, &[Key::Enter]);
assert_eq!(fake.checks.len(), 1);
assert_eq!(fake.checks[0].get("url"), Some("http://x:1"));
assert!(fake.saved.is_empty());
assert!(shown(&a).contains("ok: http://x:1 answered in 12 ms"));
fake.check_result = Some(Err("could not connect".to_owned()));
press(&mut a, &mut fake, &[Key::Enter]);
assert!(shown(&a).contains("could not connect"));
press(&mut a, &mut fake, &[Key::Ctrl('s')]);
assert_eq!(fake.saved.len(), 1);
}
#[test]
fn testing_is_refused_while_the_form_has_errors() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Char('n'), Key::Down, Key::Down]);
press(&mut a, &mut fake, &typed("nope"));
press(&mut a, &mut fake, &[Key::Down; 10]);
press(&mut a, &mut fake, &[Key::Enter]);
assert!(fake.checks.is_empty());
assert!(shown(&a).contains("Fix the fields in red before testing."));
}
#[test]
fn an_unsaved_form_is_discarded_on_a_second_escape() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Char('n')]);
press(&mut a, &mut fake, &typed("x"));
press(&mut a, &mut fake, &[Key::Esc]);
assert!(shown(&a).contains("There are unsaved changes"));
press(&mut a, &mut fake, &[Key::Esc]);
assert!(shown(&a).contains("taba-cli profiles"));
assert!(fake.saved.is_empty());
assert_eq!(a.file().names().len(), 3);
}
#[test]
fn ctrl_c_quits_from_the_form_too() {
let mut a = app();
let mut fake = Fake::default();
press(&mut a, &mut fake, &[Key::Char('n')]);
assert_eq!(a.handle(Key::Ctrl('c'), &mut fake), Flow::Quit);
}
#[test]
fn a_failed_write_keeps_the_previous_state_and_shows_the_error() {
let mut a = app();
let mut fake = Fake {
fail_save: Some("disk full".to_owned()),
..Fake::default()
};
press(&mut a, &mut fake, &[Key::Char('r'), Key::Char('y')]);
assert_eq!(a.file().names().len(), 3);
assert!(shown(&a).contains("disk full"));
press(&mut a, &mut fake, &[Key::Char('n')]);
press(&mut a, &mut fake, &typed("x"));
press(&mut a, &mut fake, &[Key::Ctrl('s')]);
assert!(shown(&a).contains("disk full") && shown(&a).contains("New profile"));
}
#[test]
fn a_terminal_smaller_than_60_by_16_shows_a_warning_instead_of_a_broken_screen() {
let a = app();
for (w, h) in [(59, 30), (80, 15), (10, 5)] {
let text = a.render(Style { color: false }, w, h).lines.join("\n");
assert!(
text.contains("Terminal too small") && text.contains("60 columns by 16 lines"),
"{w}x{h}: {text}"
);
assert!(!text.contains("taba-cli profiles"));
}
assert!(a
.render(Style { color: false }, 60, 16)
.lines
.join("\n")
.contains("taba-cli profiles"));
}
#[test]
fn colors_are_only_emitted_when_asked_for() {
let a = app();
let plain = a.render(Style { color: false }, 100, 30).lines.join("\n");
let colored = a.render(Style { color: true }, 100, 30).lines.join("\n");
assert!(!plain.contains('\x1b'));
assert!(colored.contains("\x1b[1m") && colored.contains("\x1b[0m"));
}