use super::super::*;
impl App {
pub(in crate::lookout::app) fn hide_revealed(&mut self) {
if let Some(pane) = self.secrets_pane_mut() {
pane.hide();
}
}
pub(crate) fn reveal_gate_open(&self) -> bool {
matches!(&self.body, Body::Secrets(pane) if pane.model.allow_read)
}
pub(in crate::lookout::app) fn reveal_selected(&mut self) -> Effect {
if !self.reveal_gate_open() {
self.notice = Some(Notice {
text: crate::commands::secret::HOW_TO_ALLOW_READ.to_string(),
grave: true,
});
return Effect::None;
}
let Some(pane) = self.secrets_pane_mut() else {
return Effect::None;
};
if !pane.visible_row_indices().contains(&pane.selected) {
return Effect::None;
}
let (Some(row), Some(environment)) = (
pane.model.rows.get(pane.selected).cloned(),
pane.environment().map(str::to_string),
) else {
return Effect::None;
};
pane.hide();
pane.pending_reveal = Some(row.key.clone());
Effect::RevealSecret {
store: pane.model.store.clone(),
provider_cache: pane.model.provider_cache.clone(),
row,
environment,
}
}
pub(in crate::lookout::app) fn copy_revealed(&mut self) -> Effect {
if !self.reveal_gate_open() {
self.notice = Some(Notice {
text: crate::commands::secret::HOW_TO_ALLOW_READ.to_string(),
grave: true,
});
return Effect::None;
}
let Some(value) = self
.secrets_pane_mut()
.and_then(|pane| pane.reveal.as_ref())
.map(|reveal| reveal.value.clone())
else {
return Effect::None;
};
self.notice = Some(Notice {
text: COPY_SENT_NOTICE.to_string(),
grave: false,
});
Effect::CopyToClipboard(ClipboardValue(value))
}
pub(in crate::lookout::app) fn on_revealed(
&mut self,
key: &str,
environment: &str,
value: Option<RevealedValue>,
) {
let gate_open = self.reveal_gate_open();
let until = self.now + REVEAL_HOLDS;
let Some(pane) = self.secrets_pane_mut() else {
return;
};
if pane.pending_reveal.as_deref() != Some(key) || pane.environment() != Some(environment) {
return;
}
pane.pending_reveal = None;
let Some(RevealedValue(value)) = value.filter(|_| gate_open) else {
return;
};
pane.reveal = Some(Reveal {
key: key.to_string(),
value,
until,
});
}
}
#[cfg(test)]
mod tests {
use super::super::super::*;
use super::super::testing::*;
use super::*;
use crate::lookout::secrets::SecretRow;
use crate::lookout::secrets::Source;
use crate::lookout::view::fixtures;
#[test]
fn a_reveal_that_lands_after_the_gate_closed_shows_nothing() {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_with_secrets_and_reads(dir.path(), true);
let answer = fixtures::ask_to_reveal(&mut app);
let mut shut = fixtures::secrets_model(dir.path(), false);
shut.environments = vec!["all".to_string(), "production".to_string()];
let _ = app.update(Msg::Secrets {
environment: "production".to_string(),
result: Ok(Box::new(shut)),
});
let _ = app.update(answer);
assert!(reveal_of(&app).is_none(), "the gate shut while it was read");
}
#[test]
fn a_reveal_that_lands_after_its_reason_went_away_shows_nothing() {
for (name, press) in [
("selection", KeyPress::SelectDown),
("tab", KeyPress::TabNext),
] {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_with_secrets_and_reads(dir.path(), true);
let answer = fixtures::ask_to_reveal(&mut app);
let _ = app.update(Msg::Key(press));
let _ = app.update(answer);
assert!(
reveal_of(&app).is_none(),
"{name} moved on and the answer put the value back"
);
}
}
#[test]
fn a_reveal_that_lands_after_the_pane_closed_and_reopened_shows_nothing() {
for (name, press) in [("close", KeyPress::Secrets), ("escape", KeyPress::Escape)] {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_with_secrets_and_reads(dir.path(), true);
let answer = fixtures::ask_to_reveal(&mut app);
let _ = app.update(Msg::Key(press));
let _ = app.update(Msg::Key(KeyPress::Secrets));
let _ = app.update(Msg::Secrets {
environment: "production".to_string(),
result: Ok(Box::new(fixtures::secrets_model(dir.path(), true))),
});
let _ = app.update(answer);
assert!(
reveal_of(&app).is_none(),
"{name} reopened a pane the stale answer names no pending read for"
);
}
}
#[test]
fn a_reveal_that_lands_for_another_environment_shows_nothing() {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_with_secrets_and_reads(dir.path(), true);
let Msg::Revealed { key, value, .. } = fixtures::ask_to_reveal(&mut app) else {
panic!("a reveal answers with a value");
};
let _ = app.update(Msg::Revealed {
key,
environment: "ci".to_string(),
value,
});
assert!(reveal_of(&app).is_none(), "that is another tab's value");
}
#[test]
fn v_reveals_only_when_allow_read_is_on() {
let dir = tempfile::tempdir().unwrap();
let mut shut = fixtures::app_with_secrets_and_reads(dir.path(), false);
let effect = shut.update(Msg::Key(KeyPress::Reveal));
assert_eq!(effect, Effect::None, "a shut gate does not read the store");
assert!(reveal_of(&shut).is_none(), "the gate is shut");
assert!(
shut.notice()
.is_some_and(|notice| notice.to_string().contains("allow_read")),
"and it says which gate and where"
);
let mut open = fixtures::app_with_secrets_and_reads(dir.path(), true);
let answer = fixtures::ask_to_reveal(&mut open);
let _ = open.update(answer);
assert_eq!(
reveal_of(&open).map(|reveal| reveal.key.as_str()),
Some("DB_PASSWORD")
);
assert_eq!(
reveal_of(&open).map(|reveal| reveal.value.as_str()),
Some(fixtures::REVEALED_VALUE),
"the value comes off the store, not out of the model"
);
}
#[test]
fn copying_says_it_was_sent_rather_than_that_it_arrived() {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_revealing(dir.path());
let _ = app.update(Msg::Key(KeyPress::Copy));
let notice = notice_of(&app).expect("a notice");
assert!(notice.contains("sent to the terminal"), "got {notice:?}");
assert!(
!notice.contains("copied"),
"OSC 52 is write-only and many terminals refuse it, so claiming \
success is a claim nothing can check: {notice:?}"
);
}
#[test]
fn copy_needs_a_revealed_value_rather_than_reading_the_store_behind_the_gate() {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_with_secrets_and_reads(dir.path(), false);
let _ = app.update(Msg::Key(KeyPress::Copy));
assert!(
notice_of(&app).is_some_and(|n| n.contains("allow_read")),
"copy is a reveal by another route and takes the same gate"
);
}
#[test]
fn copy_carries_the_revealed_value_to_the_effect() {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_revealing(dir.path());
let Effect::CopyToClipboard(value) = app.update(Msg::Key(KeyPress::Copy)) else {
panic!("an open gate over a revealed value copies it");
};
assert_eq!(value.0, fixtures::REVEALED_VALUE);
}
#[test]
fn a_reveal_clears_on_every_one_of_its_triggers_that_exists_yet() {
for (name, press) in [
("k", KeyPress::SelectUp),
("j", KeyPress::SelectDown),
("g", KeyPress::SelectFirst),
("G", KeyPress::SelectLast),
("shift-tab", KeyPress::TabPrev),
("tab", KeyPress::TabNext),
("escape", KeyPress::Escape),
("close", KeyPress::Secrets),
("refresh", KeyPress::Refresh),
("quit", KeyPress::Quit),
] {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_revealing(dir.path());
let _ = app.update(Msg::Key(press));
assert!(reveal_of(&app).is_none(), "{name} left the value on screen");
}
let dir = tempfile::tempdir().unwrap();
let mut timed = fixtures::app_revealing(dir.path());
let start = timed.now();
let _ = timed.update(Msg::Tick {
now: start + REVEAL_HOLDS,
});
assert!(
reveal_of(&timed).is_none(),
"the tenth second is the last one, so the value is gone by it"
);
}
#[test]
fn a_reveal_survives_the_tick_before_it_expires() {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_revealing(dir.path());
let start = app.now();
let _ = app.update(Msg::Tick {
now: start + REVEAL_HOLDS - Duration::from_millis(1),
});
assert!(
reveal_of(&app).is_some(),
"clearing early makes the countdown a lie"
);
}
#[test]
fn a_frozen_link_does_not_hold_a_value_on_screen() {
let dir = tempfile::tempdir().unwrap();
let mut app = fixtures::app_revealing(dir.path());
let start = app.now();
let _ = app.update(Msg::Frozen {
at_local: "12:00:00".to_string(),
why: fixtures::FROZEN_WHY.to_string(),
});
let _ = app.update(Msg::Tick {
now: start + REVEAL_HOLDS,
});
assert!(reveal_of(&app).is_none());
}
#[test]
fn a_key_with_no_value_in_this_tab_reveals_nothing() {
let dir = tempfile::tempdir().unwrap();
let store = dir.path().join("secrets.json");
let mut app = fixtures::app_with_secrets_and_reads(dir.path(), true);
let _ = app.update(Msg::Secrets {
environment: "production".to_string(),
result: Ok(Box::new(SecretsModel {
environments: vec!["all".to_string(), "production".to_string()],
rows: vec![SecretRow {
key: "DB_PASSWORD".to_string(),
source: Source::Operator,
in_force: None,
set_in: vec!["ci".to_string()],
byte_len: None,
readers: Vec::new(),
}],
allow_read: true,
store,
..SecretsModel::default()
})),
});
let answer = fixtures::ask_to_reveal(&mut app);
let _ = app.update(answer);
assert!(
reveal_of(&app).is_none(),
"nothing resolves here, so there is nothing to show"
);
}
}