use crate::theme;
use bombadil_core::model::Config;
use bombadil_core::store::Loaded;
use bombadil_core::validate::{self, Problem};
pub type Problems = Vec<Problem>;
pub fn load_problems(loaded: &Loaded) -> Problems {
match loaded {
Loaded::Ok(_) | Loaded::Fresh(_) => Vec::new(),
Loaded::ReadOnly { found, .. } => vec![Problem {
message: format!(
"the config file uses schema version {found}, which this build of \
Bombadil does not understand; it is shown read-only and will not \
be written back"
),
}],
Loaded::Unreadable { message, .. } => vec![Problem {
message: format!(
"the config file could not be read, so no projects are shown; \
the file has been left untouched: {message}"
),
}],
}
}
pub fn problems(config: &Config) -> Problems {
validate::validate(config)
}
pub fn view<'a>(problems: &Problems) -> iced::Element<'a, crate::app::Message> {
if problems.is_empty() {
return iced::widget::text("").into();
}
let mut list = iced::widget::column![].spacing(theme::SPACE_1);
for problem in problems {
list = list.push(
iced::widget::text(problem.message.clone())
.size(theme::BODY)
.color(theme::BOOT),
);
}
iced::widget::container(list)
.width(iced::Length::Fill)
.padding(theme::SPACE_2)
.style(theme::surface)
.into()
}
#[cfg(test)]
mod tests {
use super::*;
use bombadil_core::model::{Index, IndexAuth, IndexKind};
use bombadil_core::secrets::{InMemorySecretStore, SecretKey, SecretStore};
fn index(name: &str) -> Index {
Index {
name: name.into(),
url: "https://example.com/simple".into(),
kind: IndexKind::Extra,
default_for_new_projects: false,
auth: IndexAuth::None,
}
}
fn config_with_indexes(indexes: Vec<Index>) -> Config {
Config {
indexes,
..Config::default()
}
}
fn messages(found: &Problems) -> String {
found
.iter()
.map(|p| p.message.as_str())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn a_clean_config_has_no_problems() {
let found = problems(&Config::default());
assert!(found.is_empty(), "got {found:?}");
}
#[test]
fn a_collision_names_both_index_names() {
let config = config_with_indexes(vec![index("my-index"), index("my_index")]);
let found = problems(&config);
let joined = messages(&found);
assert!(
joined.contains("my-index") && joined.contains("my_index"),
"expected a problem naming both colliding index names; got {found:?}"
);
}
#[test]
fn problems_are_recomputed_after_a_config_change() {
let mut config = config_with_indexes(vec![index("my-index"), index("my_index")]);
let before = problems(&config);
assert!(
before.iter().any(|p| p.message.contains("my_index")),
"expected the collision problem before the fix; got {before:?}"
);
config.indexes[1].name = "other".into();
config.indexes[0].url = "ftp://example.com/simple".into();
let after = problems(&config);
assert!(
!after.iter().any(|p| p.message.contains("collide")),
"the fixed collision must not still be reported; got {after:?}"
);
assert!(
after.iter().any(|p| p.message.contains("ftp://")),
"the newly introduced URL problem must appear; got {after:?}"
);
}
#[test]
fn an_unreadable_config_is_reported_rather_than_looking_empty() {
let found = load_problems(&Loaded::Unreadable {
config: Config::default(),
message: "expected `=` at line 4".into(),
});
let joined = messages(&found);
assert!(
joined.contains("could not be read"),
"the banner must say the config could not be read; got {found:?}"
);
assert!(
joined.contains("expected `=` at line 4"),
"the banner must carry the underlying cause; got {found:?}"
);
}
#[test]
fn a_too_new_config_is_reported_with_the_version_it_declares() {
let found = load_problems(&Loaded::ReadOnly {
config: Config::default(),
found: 99,
});
let joined = messages(&found);
assert!(
joined.contains("99"),
"the banner must name the schema version found; got {found:?}"
);
assert!(
joined.contains("read-only"),
"the banner must say the file will not be written back; got {found:?}"
);
}
#[test]
fn a_cleanly_loaded_config_produces_no_load_banner() {
assert!(load_problems(&Loaded::Ok(Config::default())).is_empty());
assert!(load_problems(&Loaded::Fresh(Config::default())).is_empty());
}
#[test]
fn the_banner_never_contains_a_stored_secret() {
let store = InMemorySecretStore::new();
store
.set(
&SecretKey::Index {
name: "priv".into(),
},
"s3cr3t-token",
)
.unwrap();
let config = config_with_indexes(vec![Index {
url: "ftp://example.com/simple".into(),
..index("priv")
}]);
let found = problems(&config);
let joined = messages(&found);
assert!(
joined.contains("priv"),
"expected a problem referencing the index by name; got {found:?}"
);
assert!(
!joined.contains("s3cr3t-token"),
"a stored secret must never appear in banner text; got {found:?}"
);
}
}