use crossterm::event::{KeyCode, KeyEvent};
use seer_core::RecordType;
use crate::tui::action::FetchReq;
use crate::tui::panes::PaneOutcome;
const NAMESERVERS: [Option<&str>; 3] = [None, Some("8.8.8.8"), Some("1.1.1.1")];
const SYSTEM_LABEL: &str = "system";
pub struct DnsState {
pub ns_idx: usize,
pub custom_ns: Option<String>,
pub resolved_ip: Option<String>,
pub record_type: RecordType,
}
impl Default for DnsState {
fn default() -> Self {
Self {
ns_idx: 0,
custom_ns: None,
resolved_ip: None,
record_type: RecordType::A,
}
}
}
impl DnsState {
pub fn nameserver(&self) -> Option<String> {
match NAMESERVERS.get(self.ns_idx) {
Some(slot) => slot.map(str::to_string),
None => self.custom_ns.clone(),
}
}
pub fn slot_labels(&self) -> Vec<&str> {
NAMESERVERS
.iter()
.map(|slot| slot.unwrap_or(SYSTEM_LABEL))
.chain(self.custom_ns.as_deref())
.collect()
}
fn slot_count(&self) -> usize {
NAMESERVERS.len() + usize::from(self.custom_ns.is_some())
}
pub fn select_server(&mut self, server: String) {
match NAMESERVERS
.iter()
.position(|slot| *slot == Some(server.as_str()))
{
Some(idx) => self.ns_idx = idx,
None => {
self.custom_ns = Some(server);
self.ns_idx = NAMESERVERS.len();
}
}
}
pub fn handle_key(&mut self, key: KeyEvent, domain: Option<&str>) -> Option<PaneOutcome> {
match key.code {
KeyCode::Char('s') => {
let domain = domain?;
self.ns_idx = (self.ns_idx + 1) % self.slot_count();
Some(PaneOutcome::Fetch(FetchReq::Dns {
domain: domain.to_string(),
record_type: self.record_type,
nameserver: self.nameserver(),
}))
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
fn press(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn nameserver_mapping() {
let s = DnsState::default();
assert_eq!(s.nameserver(), None, "idx 0 = system");
let s1 = DnsState {
ns_idx: 1,
..Default::default()
};
assert_eq!(s1.nameserver(), Some("8.8.8.8".into()));
let s2 = DnsState {
ns_idx: 2,
..Default::default()
};
assert_eq!(s2.nameserver(), Some("1.1.1.1".into()));
}
#[test]
fn s_cycles_nameserver_and_returns_fetch() {
let mut state = DnsState::default();
let outcome = state.handle_key(press(KeyCode::Char('s')), Some("x.com"));
assert_eq!(state.ns_idx, 1);
assert!(
matches!(
outcome,
Some(PaneOutcome::Fetch(FetchReq::Dns {
ref nameserver,
..
})) if *nameserver == Some("8.8.8.8".to_string())
),
"expected Fetch(Dns{{ nameserver: Some(8.8.8.8) }}), got {outcome:?}",
);
}
#[test]
fn s_wraps_around() {
let mut state = DnsState {
ns_idx: 2,
..Default::default()
};
state.handle_key(press(KeyCode::Char('s')), Some("x.com"));
assert_eq!(state.ns_idx, 0);
}
#[test]
fn s_keeps_the_selected_record_type() {
let mut state = DnsState {
record_type: RecordType::MX,
..Default::default()
};
let outcome = state.handle_key(press(KeyCode::Char('s')), Some("x.com"));
assert!(matches!(
outcome,
Some(PaneOutcome::Fetch(FetchReq::Dns {
record_type: RecordType::MX,
..
}))
));
}
#[test]
fn s_without_domain_returns_none() {
let mut state = DnsState::default();
assert!(state.handle_key(press(KeyCode::Char('s')), None).is_none());
}
#[test]
fn a_fixed_server_selects_its_slot() {
let mut state = DnsState::default();
state.select_server("1.1.1.1".into());
assert_eq!(state.ns_idx, 2);
assert_eq!(state.custom_ns, None, "no extra slot for a fixed one");
assert_eq!(state.slot_labels(), ["system", "8.8.8.8", "1.1.1.1"]);
}
#[test]
fn a_custom_server_joins_the_cycle_as_a_fourth_slot() {
let mut state = DnsState::default();
state.select_server("tls://9.9.9.9".into());
assert_eq!(state.nameserver().as_deref(), Some("tls://9.9.9.9"));
assert_eq!(
state.slot_labels(),
["system", "8.8.8.8", "1.1.1.1", "tls://9.9.9.9"]
);
state.handle_key(press(KeyCode::Char('s')), Some("x.com"));
assert_eq!(state.nameserver(), None);
for _ in 0..3 {
state.handle_key(press(KeyCode::Char('s')), Some("x.com"));
}
assert_eq!(state.nameserver().as_deref(), Some("tls://9.9.9.9"));
state.select_server("9.9.9.10".into());
assert_eq!(state.slot_labels().len(), 4);
assert_eq!(state.nameserver().as_deref(), Some("9.9.9.10"));
}
#[test]
fn unowned_keys_return_none() {
let mut state = DnsState::default();
assert!(state
.handle_key(press(KeyCode::Esc), Some("x.com"))
.is_none());
assert!(state
.handle_key(press(KeyCode::Tab), Some("x.com"))
.is_none());
assert!(state
.handle_key(press(KeyCode::Char('j')), Some("x.com"))
.is_none());
assert!(state
.handle_key(press(KeyCode::Char('k')), Some("x.com"))
.is_none());
}
}