#![allow(dead_code)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::security;
#[derive(Deserialize, Default)]
struct RawFile {
#[serde(default)]
docker: Option<RawDocker>,
#[serde(default)]
connections: HashMap<String, RawConn>,
#[serde(default)]
users: HashMap<String, String>,
}
#[derive(Deserialize, Clone, Debug)]
struct RawDocker {
image: String,
#[serde(default = "default_pull")]
pull: String,
#[serde(default)]
args: Vec<String>,
}
fn default_pull() -> String {
"missing".to_string()
}
#[derive(Deserialize, Clone, Debug)]
struct RawConn {
#[serde(default)]
host: Option<String>,
#[serde(default)]
user: Option<String>,
#[serde(default = "default_port")]
port: u16,
#[serde(default)]
auth: Option<Auth>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
link: Option<String>,
#[serde(default)]
group: Option<String>,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
#[serde(tag = "type")]
pub enum Auth {
#[serde(rename = "key")]
Key {
key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cmd: Option<String>,
},
#[serde(rename = "password")]
Password,
#[serde(rename = "identity")]
Identity {
key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cmd: Option<String>,
},
}
impl Auth {
pub fn type_label(&self) -> &str {
match self {
Auth::Key { .. } => "key",
Auth::Password => "password",
Auth::Identity { .. } => "identity",
}
}
pub fn key(&self) -> Option<&str> {
match self {
Auth::Key { ref key, .. } | Auth::Identity { ref key, .. } => Some(key.as_str()),
Auth::Password => None,
}
}
pub fn cmd(&self) -> Option<&str> {
match self {
Auth::Key { ref cmd, .. } | Auth::Identity { ref cmd, .. } => cmd.as_deref(),
Auth::Password => None,
}
}
}
fn default_port() -> u16 {
22
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Layer {
Project,
User,
System,
}
impl Layer {
pub fn label(self) -> &'static str {
match self {
Self::Project => "project",
Self::User => "user",
Self::System => "system",
}
}
}
#[derive(Clone, Debug)]
pub struct Connection {
pub name: String,
pub host: String,
pub user: String,
pub port: u16,
pub auth: Auth,
pub description: String,
pub link: Option<String>,
pub group: Option<String>,
pub layer: Layer,
pub source_path: PathBuf,
pub shadowed: bool,
}
#[derive(Clone, Debug)]
pub struct UserEntry {
pub key: String,
pub value: String,
pub layer: Layer,
pub source_path: PathBuf,
pub shadowed: bool,
}
#[derive(Clone, Debug)]
pub struct DockerConfig {
pub image: String,
pub pull: String,
pub args: Vec<String>,
pub layer: Layer,
pub source_path: PathBuf,
}
#[derive(Debug)]
pub struct LayerStatus {
pub layer: Layer,
pub path: PathBuf,
pub connection_count: Option<usize>,
}
#[derive(Debug)]
pub struct LoadedConfig {
pub connections: Vec<Connection>,
pub all_connections: Vec<Connection>,
pub users: HashMap<String, UserEntry>,
pub all_users: Vec<UserEntry>,
pub docker: Option<DockerConfig>,
pub layers: [LayerStatus; 3],
pub project_dir: Option<PathBuf>,
pub warnings: Vec<security::Warning>,
pub group: Option<String>,
pub group_from_file: bool,
}
fn parse_range_pattern(name: &str) -> Option<(&str, u64, u64)> {
let bracket = name.rfind('[')?;
let prefix = &name[..bracket];
let rest = &name[bracket + 1..];
if !rest.ends_with(']') {
return None;
}
let range_str = &rest[..rest.len() - 1];
let (start_str, end_str) = range_str.split_once("..")?;
let start: u64 = start_str.parse().ok()?;
let end: u64 = end_str.parse().ok()?;
Some((prefix, start, end))
}
fn range_matches(conn_name: &str, input: &str) -> bool {
match parse_range_pattern(conn_name) {
None => false,
Some((prefix, start, end)) => {
if end < start {
return false;
}
match input.strip_prefix(prefix) {
None => false,
Some(suffix) => suffix.parse::<u64>().is_ok_and(|n| n >= start && n <= end),
}
}
}
}
impl LoadedConfig {
pub fn find(&self, name: &str) -> Option<&Connection> {
self.connections.iter().find(|c| c.name == name)
}
pub fn find_with_wildcard(&self, input: &str) -> anyhow::Result<Connection> {
use wildmatch::WildMatch;
if let Some(conn) = self.find(input) {
return Ok(conn.clone());
}
let mut matches: Vec<&Connection> = Vec::new();
for conn in &self.connections {
let is_glob = conn.name.contains('*') || conn.name.contains('?');
let matched = if is_glob {
WildMatch::new(&conn.name).matches(input)
} else {
range_matches(&conn.name, input)
};
if matched {
matches.push(conn);
}
}
match matches.len() {
0 => Err(anyhow::anyhow!("no connection named '{input}'")),
1 => {
let mut resolved = matches[0].clone();
resolved.host = if resolved.host.contains("${name}") {
resolved.host.replace("${name}", input)
} else {
input.to_string()
};
Ok(resolved)
}
_ => {
let conflict_list: Vec<String> = matches
.iter()
.map(|c| {
format!(
" '{}' (layer: {}, file: {})",
c.name,
c.layer.label(),
c.source_path.display()
)
})
.collect();
Err(anyhow::anyhow!(
"connection name '{}' matches multiple patterns:\n{}",
input,
conflict_list.join("\n")
))
}
}
}
pub fn filtered_connections(&self, group_filter: Option<&str>) -> Vec<&Connection> {
match group_filter {
Some(g) => self
.connections
.iter()
.filter(|c| c.group.as_deref() == Some(g))
.collect(),
None => self.connections.iter().collect(),
}
}
pub fn effective_group_filter<'a>(
&'a self,
all_flag: bool,
group_flag: Option<&'a str>,
) -> Option<&'a str> {
if all_flag {
return None;
}
if let Some(g) = group_flag {
return Some(g);
}
self.group.as_deref()
}
pub fn expand_user_field(
&self,
field: &str,
inline_overrides: &HashMap<String, String>,
) -> (String, Vec<String>) {
let mut result = field.to_string();
let mut warnings: Vec<String> = Vec::new();
let mut i = 0;
let chars: Vec<char> = result.chars().collect();
let mut new_result = String::new();
let s = result.clone();
let bytes = s.as_bytes();
let len = bytes.len();
while i < len {
if i + 1 < len && bytes[i] == b'$' && bytes[i + 1] == b'{' {
if let Some(close) = s[i + 2..].find('}') {
let key = &s[i + 2..i + 2 + close];
if key == "user" {
new_result.push_str("${user}");
i += 2 + close + 1;
continue;
}
if let Some(val) = inline_overrides
.get(key)
.map(|s| s.as_str())
.or_else(|| self.users.get(key).map(|e| e.value.as_str()))
{
new_result.push_str(val);
} else {
let token = format!("${{{key}}}");
warnings.push(format!(
"user field template '{}' is unresolved: no users: entry for key '{key}'",
token
));
new_result.push_str(&token);
}
i += 2 + close + 1;
continue;
}
}
new_result.push(bytes[i] as char);
i += 1;
}
let _ = chars;
result = new_result;
if result.contains("${user}") {
if let Some(val) = inline_overrides.get("user") {
result = result.replace("${user}", val);
} else {
match std::env::var("USER") {
Ok(env_user) => {
result = result.replace("${user}", &env_user);
}
Err(_) => {
warnings.push(
"user field contains '${user}' but $USER env var is unset; \
passing through unchanged"
.to_string(),
);
}
}
}
}
(result, warnings)
}
pub fn discover_groups(&self) -> Vec<crate::group::GroupEntry> {
use std::collections::BTreeMap;
let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
for conn in &self.connections {
if let Some(ref g) = conn.group {
let layer_label = conn.layer.label().to_string();
map.entry(g.clone()).or_default().push(layer_label.clone());
}
}
map.into_iter()
.map(|(name, raw_layers)| {
let mut seen = std::collections::HashSet::new();
let layers: Vec<String> = raw_layers
.into_iter()
.filter(|l| seen.insert(l.clone()))
.collect();
crate::group::GroupEntry { name, layers }
})
.collect()
}
}
pub fn load() -> Result<LoadedConfig> {
let cwd = std::env::current_dir().context("cannot determine current directory")?;
load_from(&cwd)
}
pub fn load_from(cwd: &Path) -> Result<LoadedConfig> {
let ag = crate::group::active_group()?;
let user_dir = dirs::config_dir().map(|d| d.join("yconn"));
let system_dir = PathBuf::from("/etc/yconn");
load_impl(
cwd,
ag.name.as_deref(),
ag.from_file,
user_dir.as_deref(),
&system_dir,
)
}
type RawLayer = (Vec<(String, RawConn)>, Layer, PathBuf);
type RawUserLayer = (Vec<(String, String)>, Layer, PathBuf);
pub(crate) fn load_impl(
cwd: &Path,
group: Option<&str>,
group_from_file: bool,
user_dir: Option<&Path>,
system_dir: &Path,
) -> Result<LoadedConfig> {
let mut warnings: Vec<security::Warning> = Vec::new();
let (project_dir, project_file) = upward_walk(cwd);
let user_file = user_dir.map(|d| d.join("connections.yaml"));
let system_file = system_dir.join("connections.yaml");
let proj = load_layer(project_file.as_deref(), Layer::Project, true, &mut warnings)?;
let user = load_layer(user_file.as_deref(), Layer::User, false, &mut warnings)?;
let sys = load_layer(Some(&system_file), Layer::System, false, &mut warnings)?;
let raw_layers: [RawLayer; 3] = [
(
proj.connections,
Layer::Project,
project_file
.clone()
.unwrap_or_else(|| PathBuf::from(".yconn")),
),
(
user.connections,
Layer::User,
user_file
.clone()
.unwrap_or_else(|| PathBuf::from("~/.config/yconn")),
),
(sys.connections, Layer::System, system_file.clone()),
];
let (connections, all_connections) = merge_connections(&raw_layers);
let user_layers: [RawUserLayer; 3] = [
(
proj.users,
Layer::Project,
project_file
.clone()
.unwrap_or_else(|| PathBuf::from(".yconn")),
),
(
user.users,
Layer::User,
user_file
.clone()
.unwrap_or_else(|| PathBuf::from("~/.config/yconn")),
),
(sys.users, Layer::System, system_file.clone()),
];
let (users, all_users) = merge_users(&user_layers);
if user.docker_present {
let path = user_file.as_deref().unwrap_or(Path::new("~/.config/yconn"));
warnings.push(security::check_docker_in_user_layer(path));
}
let docker = proj
.docker
.map(|d| {
docker_config(
d,
Layer::Project,
project_file.as_deref().unwrap_or(Path::new(".yconn")),
)
})
.or_else(|| {
sys.docker
.map(|d| docker_config(d, Layer::System, &system_file))
});
let layers = [
LayerStatus {
layer: Layer::Project,
path: project_file
.clone()
.unwrap_or_else(|| PathBuf::from(".yconn/connections.yaml")),
connection_count: proj.found.then_some(proj.count),
},
LayerStatus {
layer: Layer::User,
path: user_file
.clone()
.unwrap_or_else(|| PathBuf::from("~/.config/yconn/connections.yaml")),
connection_count: user.found.then_some(user.count),
},
LayerStatus {
layer: Layer::System,
path: system_file.clone(),
connection_count: sys.found.then_some(sys.count),
},
];
Ok(LoadedConfig {
connections,
all_connections,
users,
all_users,
docker,
layers,
project_dir,
warnings,
group: group.map(str::to_owned),
group_from_file,
})
}
fn validate_connections(path: &Path, connections: &HashMap<String, RawConn>) -> Result<()> {
for (name, raw) in connections {
let file = path.display();
if raw.host.is_none() {
anyhow::bail!("{file}: connection '{name}' is missing required field 'host'");
}
if raw.user.is_none() {
anyhow::bail!("{file}: connection '{name}' is missing required field 'user'");
}
if raw.auth.is_none() {
anyhow::bail!("{file}: connection '{name}' is missing required field 'auth'");
}
if raw.description.is_none() {
anyhow::bail!("{file}: connection '{name}' is missing required field 'description'");
}
}
Ok(())
}
fn default_auth() -> Auth {
Auth::Password
}
struct LayerData {
found: bool,
count: usize,
connections: Vec<(String, RawConn)>,
users: Vec<(String, String)>,
docker: Option<RawDocker>,
docker_present: bool,
}
fn load_layer(
path: Option<&Path>,
layer: Layer,
is_project: bool,
warnings: &mut Vec<security::Warning>,
) -> Result<LayerData> {
let path = match path {
Some(p) if p.exists() => p,
_ => {
return Ok(LayerData {
found: false,
count: 0,
connections: Vec::new(),
users: Vec::new(),
docker: None,
docker_present: false,
})
}
};
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
if let Some(w) = security::check_file_permissions(path) {
warnings.push(w);
}
if is_project {
warnings.extend(security::check_credential_fields(path, &content));
}
let raw: RawFile = serde_yaml::from_str(&content)
.with_context(|| format!("failed to parse {}: invalid YAML syntax", path.display()))?;
validate_connections(path, &raw.connections)?;
let docker_present = raw.docker.is_some();
let docker = if layer == Layer::User {
None
} else {
raw.docker
};
let count = raw.connections.len();
let connections: Vec<(String, RawConn)> = raw.connections.into_iter().collect();
let users: Vec<(String, String)> = raw.users.into_iter().collect();
Ok(LayerData {
found: true,
count,
connections,
users,
docker,
docker_present,
})
}
fn upward_walk(cwd: &Path) -> (Option<PathBuf>, Option<PathBuf>) {
let home = dirs::home_dir();
let mut dir = cwd.to_path_buf();
loop {
let yconn_dir = dir.join(".yconn");
let yconn_file = yconn_dir.join("connections.yaml");
if yconn_file.exists() {
return (Some(yconn_dir), Some(yconn_file));
}
let dotfile = dir.join(".connections.yaml");
if dotfile.exists() {
return (None, Some(dotfile));
}
let plain = dir.join("connections.yaml");
if plain.exists() {
return (None, Some(plain));
}
if home.as_ref().is_some_and(|h| dir == *h) {
break;
}
match dir.parent() {
Some(p) => dir = p.to_path_buf(),
None => break,
}
}
(None, None)
}
fn merge_connections(layers: &[RawLayer; 3]) -> (Vec<Connection>, Vec<Connection>) {
let mut all_raw: Vec<Connection> = Vec::new();
for (conns, layer, path) in layers {
for (name, raw) in conns {
all_raw.push(build_connection(name, raw, *layer, path, false));
}
}
let mut seen: HashMap<String, ()> = HashMap::new();
let mut active: Vec<Connection> = Vec::new();
for conn in &all_raw {
if !seen.contains_key(&conn.name) {
seen.insert(conn.name.clone(), ());
active.push(conn.clone());
}
}
let mut all: Vec<Connection> = Vec::new();
for active_conn in &active {
all.push(active_conn.clone());
for raw_conn in &all_raw {
if raw_conn.name == active_conn.name && raw_conn.layer != active_conn.layer {
let mut shadowed = raw_conn.clone();
shadowed.shadowed = true;
all.push(shadowed);
}
}
}
(active, all)
}
fn build_connection(
name: &str,
raw: &RawConn,
layer: Layer,
path: &Path,
shadowed: bool,
) -> Connection {
Connection {
name: name.to_string(),
host: raw.host.clone().unwrap_or_default(),
user: raw.user.clone().unwrap_or_default(),
port: raw.port,
auth: raw.auth.clone().unwrap_or_else(default_auth),
description: raw.description.clone().unwrap_or_default(),
link: raw.link.clone(),
group: raw.group.clone(),
layer,
source_path: path.to_path_buf(),
shadowed,
}
}
fn merge_users(layers: &[RawUserLayer; 3]) -> (HashMap<String, UserEntry>, Vec<UserEntry>) {
let mut all_raw: Vec<UserEntry> = Vec::new();
for (entries, layer, path) in layers {
for (key, value) in entries {
all_raw.push(UserEntry {
key: key.clone(),
value: value.clone(),
layer: *layer,
source_path: path.clone(),
shadowed: false,
});
}
}
let mut seen: HashMap<String, ()> = HashMap::new();
let mut active: HashMap<String, UserEntry> = HashMap::new();
let mut active_order: Vec<String> = Vec::new();
for entry in &all_raw {
if !seen.contains_key(&entry.key) {
seen.insert(entry.key.clone(), ());
active.insert(entry.key.clone(), entry.clone());
active_order.push(entry.key.clone());
}
}
let mut all: Vec<UserEntry> = Vec::new();
for key in &active_order {
let active_entry = active.get(key).unwrap();
all.push(active_entry.clone());
for raw_entry in &all_raw {
if raw_entry.key == *key && raw_entry.layer != active_entry.layer {
let mut shadowed = raw_entry.clone();
shadowed.shadowed = true;
all.push(shadowed);
}
}
}
(active, all)
}
fn docker_config(raw: RawDocker, layer: Layer, path: &Path) -> DockerConfig {
DockerConfig {
image: raw.image,
pull: raw.pull,
args: raw.args,
layer,
source_path: path.to_path_buf(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_yaml(dir: &Path, filename: &str, content: &str) -> PathBuf {
let path = dir.join(filename);
fs::write(&path, content).unwrap();
path
}
fn simple_conn(name: &str, host: &str) -> String {
format!(
"connections:\n {name}:\n host: {host}\n user: user\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: desc\n"
)
}
fn conn_with_group(name: &str, host: &str, group: &str) -> String {
format!(
"connections:\n {name}:\n host: {host}\n user: user\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: desc\n group: {group}\n"
)
}
fn load_test(cwd: &Path, user_dir: Option<&Path>, system_dir: &Path) -> LoadedConfig {
load_impl(cwd, None, false, user_dir, system_dir).unwrap()
}
fn load_test_with_group(
cwd: &Path,
user_dir: Option<&Path>,
system_dir: &Path,
group: Option<&str>,
) -> LoadedConfig {
load_impl(cwd, group, group.is_some(), user_dir, system_dir).unwrap()
}
#[test]
fn test_upward_walk_finds_at_root() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(&yconn, "connections.yaml", &simple_conn("srv", "1.2.3.4"));
let nested = root.path().join("a").join("b").join("c");
fs::create_dir_all(&nested).unwrap();
let (dir, file) = upward_walk(&nested);
assert_eq!(dir.unwrap(), yconn);
assert!(file.unwrap().exists());
}
#[test]
fn test_upward_walk_no_config() {
let dir = TempDir::new().unwrap();
let (d, f) = upward_walk(dir.path());
assert!(d.is_none());
assert!(f.is_none());
}
#[test]
fn test_upward_walk_finds_dotfile_convention() {
let root = TempDir::new().unwrap();
let dotfile = root.path().join(".connections.yaml");
fs::write(&dotfile, simple_conn("srv", "1.2.3.4")).unwrap();
let nested = root.path().join("sub");
fs::create_dir_all(&nested).unwrap();
let (dir, file) = upward_walk(&nested);
assert!(dir.is_none(), "no .yconn dir for dotfile convention");
assert_eq!(file.unwrap(), dotfile);
}
#[test]
fn test_upward_walk_finds_plain_convention() {
let root = TempDir::new().unwrap();
let plain = root.path().join("connections.yaml");
fs::write(&plain, simple_conn("srv", "1.2.3.4")).unwrap();
let nested = root.path().join("sub");
fs::create_dir_all(&nested).unwrap();
let (dir, file) = upward_walk(&nested);
assert!(dir.is_none(), "no .yconn dir for plain convention");
assert_eq!(file.unwrap(), plain);
}
#[test]
fn test_upward_walk_yconn_beats_dotfile_same_dir() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&simple_conn("yconn-srv", "1.1.1.1"),
);
fs::write(
root.path().join(".connections.yaml"),
simple_conn("dotfile-srv", "2.2.2.2"),
)
.unwrap();
let (_, file) = upward_walk(root.path());
let content = fs::read_to_string(file.unwrap()).unwrap();
assert!(
content.contains("yconn-srv"),
".yconn convention must beat dotfile"
);
}
#[test]
fn test_upward_walk_dotfile_beats_plain_same_dir() {
let root = TempDir::new().unwrap();
fs::write(
root.path().join(".connections.yaml"),
simple_conn("dotfile-srv", "2.2.2.2"),
)
.unwrap();
fs::write(
root.path().join("connections.yaml"),
simple_conn("plain-srv", "3.3.3.3"),
)
.unwrap();
let (_, file) = upward_walk(root.path());
let content = fs::read_to_string(file.unwrap()).unwrap();
assert!(
content.contains("dotfile-srv"),
"dotfile convention must beat plain"
);
}
#[test]
fn test_upward_walk_finds_closest_ancestor() {
let root = TempDir::new().unwrap();
let outer_yconn = root.path().join(".yconn");
fs::create_dir_all(&outer_yconn).unwrap();
write_yaml(
&outer_yconn,
"connections.yaml",
&simple_conn("outer", "1.1.1.1"),
);
let inner = root.path().join("inner");
let inner_yconn = inner.join(".yconn");
fs::create_dir_all(&inner_yconn).unwrap();
write_yaml(
&inner_yconn,
"connections.yaml",
&simple_conn("inner", "2.2.2.2"),
);
let (_, file) = upward_walk(&inner);
let content = fs::read_to_string(file.unwrap()).unwrap();
assert!(content.contains("inner"));
}
#[test]
fn test_single_project_layer() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(&yconn, "connections.yaml", &simple_conn("prod", "10.0.0.1"));
let empty = TempDir::new().unwrap();
let cfg = load_test(root.path(), None, empty.path());
assert_eq!(cfg.connections.len(), 1);
assert_eq!(cfg.connections[0].name, "prod");
assert_eq!(cfg.connections[0].layer, Layer::Project);
}
#[test]
fn test_single_user_layer() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("my-box", "192.168.1.5"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
assert_eq!(cfg.connections.len(), 1);
assert_eq!(cfg.connections[0].name, "my-box");
assert_eq!(cfg.connections[0].layer, Layer::User);
}
#[test]
fn test_single_system_layer() {
let cwd = TempDir::new().unwrap();
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("bastion", "10.0.0.254"),
);
let cfg = load_test(cwd.path(), None, sys.path());
assert_eq!(cfg.connections.len(), 1);
assert_eq!(cfg.connections[0].layer, Layer::System);
}
#[test]
fn test_project_overrides_user() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&simple_conn("srv", "project-host"),
);
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "user-host"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(root.path(), Some(user.path()), empty.path());
let conn = cfg.find("srv").unwrap();
assert_eq!(conn.host, "project-host");
assert_eq!(conn.layer, Layer::Project);
}
#[test]
fn test_project_overrides_system() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&simple_conn("srv", "project-host"),
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("srv", "system-host"),
);
let cfg = load_test(root.path(), None, sys.path());
assert_eq!(cfg.find("srv").unwrap().host, "project-host");
}
#[test]
fn test_user_overrides_system() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "user-host"),
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("srv", "system-host"),
);
let cfg = load_test(cwd.path(), Some(user.path()), sys.path());
assert_eq!(cfg.find("srv").unwrap().host, "user-host");
assert_eq!(cfg.find("srv").unwrap().layer, Layer::User);
}
#[test]
fn test_project_overrides_both() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&simple_conn("srv", "project-host"),
);
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "user-host"),
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("srv", "system-host"),
);
let cfg = load_test(root.path(), Some(user.path()), sys.path());
assert_eq!(cfg.find("srv").unwrap().host, "project-host");
}
#[test]
fn test_no_collision_all_layers() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&simple_conn("proj-srv", "1.0.0.1"),
);
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("user-srv", "2.0.0.1"),
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("sys-srv", "3.0.0.1"),
);
let cfg = load_test(root.path(), Some(user.path()), sys.path());
assert_eq!(cfg.connections.len(), 3);
assert!(cfg.find("proj-srv").is_some());
assert!(cfg.find("user-srv").is_some());
assert!(cfg.find("sys-srv").is_some());
}
#[test]
fn test_name_only_in_system() {
let cwd = TempDir::new().unwrap();
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("sys-only", "10.0.0.1"),
);
let cfg = load_test(cwd.path(), None, sys.path());
let conn = cfg.find("sys-only").unwrap();
assert_eq!(conn.layer, Layer::System);
}
#[test]
fn test_name_only_in_user() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("user-only", "10.0.0.2"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find("user-only").unwrap();
assert_eq!(conn.layer, Layer::User);
}
#[test]
fn test_missing_layer_silently_skipped() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "1.2.3.4"),
);
let sys = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), sys.path());
assert_eq!(cfg.connections.len(), 1);
assert!(cfg.layers[2].connection_count.is_none());
}
#[test]
fn test_shadowed_entries_in_all_connections() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&simple_conn("srv", "project-host"),
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("srv", "system-host"),
);
let cfg = load_test(root.path(), None, sys.path());
assert_eq!(cfg.connections.len(), 1);
assert_eq!(cfg.all_connections.len(), 2);
let active = cfg.all_connections.iter().find(|c| !c.shadowed).unwrap();
assert_eq!(active.host, "project-host");
let shadowed = cfg.all_connections.iter().find(|c| c.shadowed).unwrap();
assert_eq!(shadowed.host, "system-host");
assert_eq!(shadowed.layer, Layer::System);
}
#[test]
fn test_shadowed_entry_interleaved_after_active() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&format!(
"{}\n{}",
simple_conn("alpha", "1.0.0.1"),
" beta:\n host: 2.0.0.2\n user: u\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: d\n"
),
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("alpha", "3.0.0.3"),
);
let cfg = load_test(root.path(), None, sys.path());
let shadowed_idx = cfg.all_connections.iter().position(|c| c.shadowed).unwrap();
let active_idx = cfg
.all_connections
.iter()
.position(|c| c.name == "alpha" && !c.shadowed)
.unwrap();
assert_eq!(shadowed_idx, active_idx + 1);
}
#[test]
fn test_docker_from_project_layer() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"docker:\n image: ghcr.io/org/keys:latest\nconnections: {}\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(root.path(), None, empty.path());
let docker = cfg.docker.unwrap();
assert_eq!(docker.image, "ghcr.io/org/keys:latest");
assert_eq!(docker.layer, Layer::Project);
}
#[test]
fn test_docker_from_system_layer() {
let cwd = TempDir::new().unwrap();
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
"docker:\n image: registry/img:v1\nconnections: {}\n",
);
let cfg = load_test(cwd.path(), None, sys.path());
assert!(cfg.docker.is_some());
assert_eq!(cfg.docker.unwrap().layer, Layer::System);
}
#[test]
fn test_docker_project_takes_priority_over_system() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"docker:\n image: project-image\nconnections: {}\n",
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
"docker:\n image: system-image\nconnections: {}\n",
);
let cfg = load_test(root.path(), None, sys.path());
assert_eq!(cfg.docker.unwrap().image, "project-image");
}
#[test]
fn test_docker_in_user_layer_ignored_with_warning() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"docker:\n image: bad-image\nconnections: {}\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
assert!(cfg.docker.is_none());
assert!(!cfg.warnings.is_empty());
assert!(cfg.warnings.iter().any(|w| w.message.contains("docker")));
}
#[test]
fn test_no_docker_block() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "1.2.3.4"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
assert!(cfg.docker.is_none());
}
#[test]
fn test_docker_pull_defaults_to_missing() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"docker:\n image: img\nconnections: {}\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(root.path(), None, empty.path());
assert_eq!(cfg.docker.unwrap().pull, "missing");
}
#[test]
fn test_port_defaults_to_22() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "1.2.3.4"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
assert_eq!(cfg.connections[0].port, 22);
}
#[test]
fn test_layer_status_counts() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n a:\n host: h\n user: u\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: d\n b:\n host: h2\n user: u2\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: d2\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
assert_eq!(cfg.layers[0].connection_count, None); assert_eq!(cfg.layers[1].connection_count, Some(2)); assert_eq!(cfg.layers[2].connection_count, None); }
#[test]
fn test_group_field_round_trip() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&conn_with_group("work-srv", "10.0.0.1", "work"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find("work-srv").unwrap();
assert_eq!(conn.group.as_deref(), Some("work"));
}
#[test]
fn test_group_field_absent_is_none() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "1.2.3.4"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find("srv").unwrap();
assert!(conn.group.is_none());
}
#[test]
fn test_filtered_connections_no_filter() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
let yaml = format!(
"{}\n{}",
conn_with_group("work-srv", "10.0.0.1", "work"),
" plain-srv:\n host: 10.0.0.2\n user: user\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: desc\n"
);
write_yaml(user.path(), "connections.yaml", &yaml);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let filtered = cfg.filtered_connections(None);
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_filtered_connections_with_group_filter() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
let yaml = format!(
"{}\n{}",
conn_with_group("work-srv", "10.0.0.1", "work"),
" plain-srv:\n host: 10.0.0.2\n user: user\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: desc\n"
);
write_yaml(user.path(), "connections.yaml", &yaml);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let filtered = cfg.filtered_connections(Some("work"));
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "work-srv");
}
#[test]
fn test_effective_group_filter_all_overrides_everything() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load_test_with_group(cwd.path(), None, empty.path(), Some("work"));
assert_eq!(cfg.effective_group_filter(true, None), None);
}
#[test]
fn test_effective_group_filter_group_flag_overrides_lock() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load_test_with_group(cwd.path(), None, empty.path(), Some("work"));
assert_eq!(
cfg.effective_group_filter(false, Some("private")),
Some("private")
);
}
#[test]
fn test_effective_group_filter_locked_group_used() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load_test_with_group(cwd.path(), None, empty.path(), Some("work"));
assert_eq!(cfg.effective_group_filter(false, None), Some("work"));
}
#[test]
fn test_effective_group_filter_no_lock_no_flags() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), None, empty.path());
assert_eq!(cfg.effective_group_filter(false, None), None);
}
#[test]
fn test_discover_groups_empty() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), None, empty.path());
let groups = cfg.discover_groups();
assert!(groups.is_empty());
}
#[test]
fn test_discover_groups_from_connections() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
let yaml = format!(
"{}\n{}",
conn_with_group("work-srv", "10.0.0.1", "work"),
" private-srv:\n host: 10.0.0.2\n user: user\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: desc\n group: private\n"
);
write_yaml(user.path(), "connections.yaml", &yaml);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let groups = cfg.discover_groups();
assert_eq!(groups.len(), 2);
let names: Vec<&str> = groups.iter().map(|g| g.name.as_str()).collect();
assert!(names.contains(&"work"));
assert!(names.contains(&"private"));
}
#[test]
fn test_discover_groups_sorted_by_name() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
let yaml = format!(
"{}\n{}",
conn_with_group("z-srv", "10.0.0.1", "zebra"),
" a-srv:\n host: 10.0.0.2\n user: user\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: desc\n group: alpha\n"
);
write_yaml(user.path(), "connections.yaml", &yaml);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let groups = cfg.discover_groups();
let names: Vec<&str> = groups.iter().map(|g| g.name.as_str()).collect();
assert_eq!(names, vec!["alpha", "zebra"]);
}
#[test]
fn test_discover_groups_tracks_layers() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&conn_with_group("p-srv", "10.0.0.1", "work"),
);
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&conn_with_group("u-srv", "10.0.0.2", "work"),
);
let empty = TempDir::new().unwrap();
let cfg = load_test(root.path(), Some(user.path()), empty.path());
let groups = cfg.discover_groups();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].name, "work");
let layers = &groups[0].layers;
assert!(layers.contains(&"project".to_string()));
assert!(layers.contains(&"user".to_string()));
}
#[test]
fn test_wildcard_single_pattern_matches() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n web-*:\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Wildcard web\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("web-prod").unwrap();
assert_eq!(conn.host, "web-prod");
assert_eq!(conn.user, "deploy");
assert_eq!(conn.name, "web-*");
}
#[test]
fn test_wildcard_no_match_returns_error() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n web-*:\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Wildcard web\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let err = cfg.find_with_wildcard("db-prod").unwrap_err();
assert!(
err.to_string().contains("db-prod"),
"error must name the input: {err}"
);
}
#[test]
fn test_wildcard_conflict_two_patterns_same_input() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n web-*:\n host: ph1\n user: deploy\n auth:\n type: password\n description: Web wildcard\n \"?eb-prod\":\n host: ph2\n user: admin\n auth:\n type: password\n description: Prefix wildcard\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let err = cfg.find_with_wildcard("web-prod").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("web-*"),
"error must name pattern 'web-*': {msg}"
);
assert!(
msg.contains("?eb-prod"),
"error must name pattern '?eb-prod': {msg}"
);
}
#[test]
fn test_wildcard_exact_name_beats_pattern() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n web-prod:\n host: exact-host\n user: exact-user\n auth:\n type: password\n description: Exact match\n web-*:\n host: wildcard-host\n user: wildcard-user\n auth:\n type: password\n description: Wildcard\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("web-prod").unwrap();
assert_eq!(conn.host, "exact-host");
assert_eq!(conn.user, "exact-user");
assert_eq!(conn.name, "web-prod");
}
#[test]
fn test_wildcard_same_pattern_in_two_layers_is_shadowing_not_conflict() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"connections:\n host-*:\n host: proj-host\n user: project-user\n auth:\n type: password\n description: Project wildcard\n",
);
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n host-*:\n host: user-host\n user: user-user\n auth:\n type: password\n description: User wildcard\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(root.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("host-anything").unwrap();
assert_eq!(conn.host, "host-anything", "input must replace host");
assert_eq!(conn.user, "project-user", "project layer must win");
}
#[test]
fn test_wildcard_question_mark_single_char() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n web-?:\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Single char wildcard\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("web-1").unwrap();
assert_eq!(conn.host, "web-1");
let err = cfg.find_with_wildcard("web-12").unwrap_err();
assert!(err.to_string().contains("web-12"));
}
#[test]
fn test_wildcard_host_with_name_template_is_expanded() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n server*:\n host: \"${name}.corp.com\"\n user: deploy\n auth:\n type: password\n description: Corp servers\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("server01").unwrap();
assert_eq!(conn.host, "server01.corp.com");
}
#[test]
fn test_wildcard_host_without_name_template_replaced_by_input() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n web-*:\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Web servers\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("web-prod").unwrap();
assert_eq!(conn.host, "web-prod");
}
#[test]
fn test_wildcard_exact_match_name_template_not_expanded() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n myconn:\n host: \"${name}.corp.com\"\n user: deploy\n auth:\n type: password\n description: My connection\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("myconn").unwrap();
assert_eq!(conn.host, "${name}.corp.com");
}
#[test]
fn test_range_matches_lower_bound() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Range servers\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("server1").unwrap();
assert_eq!(conn.host, "server1");
}
#[test]
fn test_range_matches_upper_bound() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Range servers\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("server10").unwrap();
assert_eq!(conn.host, "server10");
}
#[test]
fn test_range_matches_midpoint() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Range servers\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("server5").unwrap();
assert_eq!(conn.host, "server5");
}
#[test]
fn test_range_outside_range_does_not_match() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: placeholder\n user: deploy\n auth:\n type: password\n description: Range servers\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let err = cfg.find_with_wildcard("server11").unwrap_err();
assert!(err.to_string().contains("server11"));
let err0 = cfg.find_with_wildcard("server0").unwrap_err();
assert!(err0.to_string().contains("server0"));
}
#[test]
fn test_range_conflict_with_glob_pattern() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: ph1\n user: deploy\n auth:\n type: password\n description: Range\n server*:\n host: ph2\n user: admin\n auth:\n type: password\n description: Glob\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let err = cfg.find_with_wildcard("server5").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("server[1..10]"),
"must name range pattern: {msg}"
);
assert!(msg.contains("server*"), "must name glob pattern: {msg}");
}
#[test]
fn test_range_exact_name_beats_matching_range() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n server5:\n host: exact-host\n user: exact-user\n auth:\n type: password\n description: Exact\n \"server[1..10]\":\n host: range-host\n user: range-user\n auth:\n type: password\n description: Range\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("server5").unwrap();
assert_eq!(conn.host, "exact-host");
assert_eq!(conn.user, "exact-user");
}
#[test]
fn test_range_same_pattern_in_two_layers_is_shadowing_not_conflict() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: proj-host\n user: project-user\n auth:\n type: password\n description: Project range\n",
);
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: user-host\n user: user-user\n auth:\n type: password\n description: User range\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(root.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("server5").unwrap();
assert_eq!(conn.user, "project-user");
}
#[test]
fn test_range_with_name_template_expands_host() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n \"server[1..10]\":\n host: \"${name}.corp.com\"\n user: deploy\n auth:\n type: password\n description: Corp servers\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
let conn = cfg.find_with_wildcard("server5").unwrap();
assert_eq!(conn.host, "server5.corp.com");
}
#[test]
fn test_identity_connection_parsed_correctly() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n github:\n host: github.com\n user: git\n auth:\n type: identity\n key: ~/.ssh/github_key\n description: GitHub identity\n",
);
let empty = TempDir::new().unwrap();
let cfg = load_test(cwd.path(), Some(user.path()), empty.path());
assert_eq!(cfg.connections.len(), 1);
let conn = &cfg.connections[0];
assert_eq!(conn.name, "github");
assert_eq!(conn.auth.type_label(), "identity");
assert_eq!(conn.auth.key(), Some("~/.ssh/github_key"));
}
#[test]
fn test_identity_without_key_rejected() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
"connections:\n github:\n host: github.com\n user: git\n auth:\n type: identity\n description: GitHub identity\n",
);
let empty = TempDir::new().unwrap();
let result = load_impl(cwd.path(), None, false, Some(user.path()), empty.path());
assert!(
result.is_err(),
"identity auth without key should be rejected"
);
}
}