use super::super::*;
impl App {
pub(in crate::lookout::app) fn secrets_confirm(&mut self) -> Effect {
let is_new_key_row = matches!(
&self.body,
Body::Secrets(pane)
if !pane.model.environments.is_empty() && pane.selected_is_new_key_row()
);
if is_new_key_row {
if self.authorize_write().is_none() {
return Effect::None;
}
let Some(pane) = self.secrets_pane_mut() else {
return Effect::None;
};
pane.typing = Some(Typing {
what: TypingWhat::NewKey,
buffer: TextBox::new(),
});
self.mode = InputMode::Text;
return Effect::None;
}
let Some(row) = (match &self.body {
Body::Secrets(pane) => pane.model.rows.get(pane.selected).cloned(),
Body::FlockTable
| Body::Settings(_)
| Body::ConfigPane(_)
| Body::Bleats(_)
| Body::Sheep(_) => None,
}) else {
return Effect::None;
};
if self.refuse_provider_row(&row) {
return Effect::None;
}
if self.authorize_write().is_none() {
return Effect::None;
}
let Some(pane) = self.secrets_pane_mut() else {
return Effect::None;
};
pane.typing = Some(Typing {
what: TypingWhat::ValueFor(row.key),
buffer: TextBox::new(),
});
self.mode = InputMode::Text;
Effect::None
}
pub(in crate::lookout::app) fn on_secrets_text_key(&mut self, key: KeyPress) -> Effect {
match key {
KeyPress::Quit => return Effect::Quit,
KeyPress::TextChar(typed) => {
if let Some(typing) = self
.secrets_pane_mut()
.and_then(|pane| pane.typing.as_mut())
{
typing.buffer.insert(typed);
}
}
KeyPress::TextBackspace => {
if let Some(typing) = self
.secrets_pane_mut()
.and_then(|pane| pane.typing.as_mut())
{
typing.buffer.backspace();
}
}
KeyPress::TextEdit(edit) => {
if let Some(typing) = self
.secrets_pane_mut()
.and_then(|pane| pane.typing.as_mut())
{
typing.buffer.edit(edit);
}
}
KeyPress::TextApply => return self.apply_secrets_text(),
KeyPress::TextAbandon => {
if let Some(pane) = self.secrets_pane_mut() {
pane.typing = None;
}
self.mode = InputMode::Normal;
}
_ => {}
}
Effect::None
}
fn apply_secrets_text(&mut self) -> Effect {
let Some(typing) = self.secrets_pane_mut().and_then(|pane| pane.typing.take()) else {
return Effect::None;
};
match typing.what {
TypingWhat::NewKey => {
let name = typing.buffer;
if shep_core::secrets::is_name(name.as_str()) {
if let Some(pane) = self.secrets_pane_mut() {
pane.typing = Some(Typing {
what: TypingWhat::ValueFor(name.as_str().to_string()),
buffer: TextBox::new(),
});
}
return Effect::None;
}
self.notice = Some(Notice::grave(format!(
"{:?} is not a valid key: {NEW_KEY_GRAMMAR}",
name.as_str()
)));
if let Some(pane) = self.secrets_pane_mut() {
pane.typing = Some(Typing {
what: TypingWhat::NewKey,
buffer: name,
});
}
Effect::None
}
TypingWhat::ValueFor(key) => {
let value = typing.buffer;
if value.as_str().len() > shep_core::secrets::MAX_VALUE_BYTES {
self.notice = Some(Notice::grave(format!(
"value is {} bytes, over the {}-byte limit",
value.as_str().len(),
shep_core::secrets::MAX_VALUE_BYTES
)));
if let Some(pane) = self.secrets_pane_mut() {
pane.typing = Some(Typing {
what: TypingWhat::ValueFor(key),
buffer: value,
});
}
return Effect::None;
}
let environment = match &self.body {
Body::Secrets(pane) => pane.environment().unwrap_or_default().to_string(),
Body::FlockTable
| Body::Settings(_)
| Body::ConfigPane(_)
| Body::Bleats(_)
| Body::Sheep(_) => return Effect::None,
};
let Some(authority) = self.authorize_write() else {
return Effect::None;
};
self.mode = InputMode::Normal;
Effect::WriteSecret(
SecretEdit {
key,
environment,
value: Some(value.as_str().to_string()),
},
authority,
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::testing::*;
use crate::lookout::view::fixtures;
#[test]
fn enter_opens_the_value_input_seeded_empty() {
let mut app = fixtures::app_with_secrets_and_control();
let _ = app.update(Msg::Key(KeyPress::Confirm));
let typing = typing_of(&app).expect("the input is open");
assert_eq!(typing.what, TypingWhat::ValueFor("DB_PASSWORD".into()));
assert_eq!(
typing.buffer.as_str(),
"",
"seeding it with the stored value would put a secret on screen \
that `v` and its gate exist to control"
);
}
#[test]
fn a_write_refuses_without_the_control_gate() {
let mut app = fixtures::app_with_secrets_read_only();
let _ = app.update(Msg::Key(KeyPress::Confirm));
assert!(typing_of(&app).is_none());
assert!(
notice_of(&app).is_some_and(|n| n.contains("read-only")),
"the existing refusal, not a second one"
);
}
#[test]
fn a_value_over_the_cap_is_refused_at_the_input_not_at_the_file() {
let mut app = fixtures::app_typing_a_value();
for _ in 0..=shep_core::secrets::MAX_VALUE_BYTES {
let _ = app.update(Msg::Key(KeyPress::TextChar('x')));
}
let effect = app.update(Msg::Key(KeyPress::TextApply));
assert!(matches!(effect, Effect::None), "nothing reached the file");
assert!(notice_of(&app).is_some_and(|n| n.contains("4096")));
}
#[test]
fn a_key_outside_the_grammar_is_refused_with_the_grammar() {
let mut app = fixtures::app_typing_a_new_key();
for c in ".bad".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(c)));
}
let effect = app.update(Msg::Key(KeyPress::TextApply));
assert!(matches!(effect, Effect::None));
assert!(
notice_of(&app).is_some_and(|n| n.contains("not starting with a dot")),
"the refusal states the rule, not just that it failed"
);
}
#[test]
fn a_value_lands_in_the_tabs_own_environment() {
let mut app = fixtures::app_with_secrets_and_control();
let _ = app.update(Msg::Key(KeyPress::TabNext));
let _ = app.update(Msg::Key(KeyPress::Confirm));
for c in "s3cret".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(c)));
}
let effect = app.update(Msg::Key(KeyPress::TextApply));
let Effect::WriteSecret(edit, _) = effect else {
panic!("expected a write, got {effect:?}");
};
assert_eq!(edit.environment, second_tab_of(&app));
assert_eq!(edit.value.as_deref(), Some("s3cret"));
}
#[test]
fn a_successful_write_takes_a_revealed_value_off_the_screen() {
let mut app = fixtures::app_revealing_with_control();
let _ = app.update(Msg::SecretWritten { result: Ok(true) });
assert!(
reveal_of(&app).is_none(),
"the value on screen belonged to what the store held before the write"
);
}
#[test]
fn a_failed_write_says_why_and_leaves_the_table_alone() {
let mut app = fixtures::app_with_secrets_and_control();
let before = row_count(&app);
let _ = app.update(Msg::SecretWritten {
result: Err("permission denied (os error 13)".to_string()),
});
assert!(
notice_of(&app).is_some_and(|n| n.contains("permission denied")),
"the operator gets the reason, not a silent no-op"
);
assert_eq!(row_count(&app), before, "and nothing is redrawn as changed");
}
#[test]
fn the_new_key_row_opens_a_name_input_and_then_a_value_input() {
let mut app = fixtures::app_with_secrets_and_control();
select_new_key_row(&mut app);
let _ = app.update(Msg::Key(KeyPress::Confirm));
assert_eq!(
typing_of(&app).map(|t| t.what.clone()),
Some(TypingWhat::NewKey)
);
for c in "NEW_KEY".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(c)));
}
let effect = app.update(Msg::Key(KeyPress::TextApply));
assert!(
matches!(effect, Effect::None),
"naming a key writes nothing on its own"
);
assert_eq!(
typing_of(&app).map(|t| t.what.clone()),
Some(TypingWhat::ValueFor("NEW_KEY".into())),
"the name input hands straight over to the value input"
);
}
#[test]
fn a_provider_row_refuses_a_write() {
let mut app = fixtures::app_with_a_pushed_secret_selected_and_control();
let _ = app.update(Msg::Key(KeyPress::Confirm));
assert!(typing_of(&app).is_none());
assert!(
notice_of(&app).is_some_and(|n| n.contains("pushed by a dog")),
"and it says why rather than doing nothing"
);
}
#[test]
fn a_pasted_value_is_never_printed_in_a_notice_or_a_debug() {
let mut app = fixtures::app_typing_a_value();
let paste = Msg::Paste(Pasted::new("hunter2\nsecond".to_string()));
assert_eq!(format!("{paste:?}"), "Paste(Pasted(<14 bytes>))");
let _ = app.update(paste);
let typing = typing_of(&app).expect("the newline did not apply the input");
assert_eq!(typing.buffer.as_str(), "hunter2second");
assert_eq!(
format!("{typing:?}"),
"Typing { what: ValueFor(\"DB_PASSWORD\"), buffer: <13 bytes> }"
);
assert_eq!(notice_of(&app), None);
}
#[test]
fn a_secret_value_can_be_corrected_at_the_cursor() {
let mut app = fixtures::app_typing_a_value();
for typed in "xz".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextEdit(TextEdit::Left)));
let _ = app.update(Msg::Key(KeyPress::TextChar('y')));
assert_eq!(typing_of(&app).unwrap().buffer.as_str(), "xyz");
}
}