use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use serde_json::{Value, json};
use super::*;
use crate::test_support::isolated_daemon_home;
use crate::uds_mock::{self, MockFuture, RpcError};
fn resolved_socket() -> PathBuf {
trusty_common::daemon_socket_path("trusty-search").expect("derive the trusty-search socket")
}
type Recorder = Arc<Mutex<Vec<(String, Value)>>>;
fn recording(seen: Recorder, result: Value) -> impl Fn(&str, Value) -> MockFuture + Send + Sync {
move |method, params| {
seen.lock()
.expect("recorder lock")
.push((method.to_string(), params));
let result = result.clone();
Box::pin(async move { Ok(result) })
}
}
fn refusing(code: i64, message: &str) -> impl Fn(&str, Value) -> MockFuture + Send + Sync {
let message = message.to_string();
move |_method, _params| {
let error = RpcError::new(code, message.clone());
Box::pin(async move { Err(error) })
}
}
#[serial_test::serial]
#[test]
fn acquire_is_refused_under_a_test_harness() {
assert!(
DestructiveIndexDelete::acquire().is_none(),
"a cargo test process must never acquire the destructive-delete capability (#4743)"
);
}
#[serial_test::serial]
#[test]
fn acquire_refuses_when_no_daemon_socket_is_bound() {
let (_dir, _scope) = isolated_daemon_home(true);
assert!(
DestructiveIndexDelete::acquire().is_none(),
"no bound socket means no daemon to delete through"
);
}
#[serial_test::serial]
#[tokio::test]
async fn acquire_succeeds_when_production_state_is_explicitly_allowed() {
let (_dir, _scope) = isolated_daemon_home(true);
let socket = resolved_socket();
let _daemon = uds_mock::spawn_at(socket.clone(), uds_mock::always(json!({}))).await;
let acquired = DestructiveIndexDelete::acquire().expect("the explicit opt-in must yield one");
assert_eq!(
acquired.socket, socket,
"the capability must point at the resolved socket"
);
}
#[serial_test::serial]
#[tokio::test]
async fn delete_params_opt_into_data_deletion() {
let (_dir, _scope) = isolated_daemon_home(true);
let seen: Recorder = Arc::new(Mutex::new(Vec::new()));
let _daemon = uds_mock::spawn_at(
resolved_socket(),
recording(Arc::clone(&seen), json!({ "removed": true })),
)
.await;
let cap = DestructiveIndexDelete::acquire().expect("capability");
cap.delete("my-index").await;
let calls = seen.lock().expect("recorder lock").clone();
assert_eq!(
calls,
vec![(
"search.index.delete".to_string(),
json!({ "index_id": "my-index", "delete_data": true })
)],
"the destructive call must name trusty-search's own method and carry the opt-in"
);
}
#[serial_test::serial]
#[tokio::test]
async fn delete_over_a_stale_socket_is_a_transport_failure() {
let (_dir, _scope) = isolated_daemon_home(true);
let socket = resolved_socket();
std::fs::create_dir_all(socket.parent().expect("socket parent"))
.expect("create the daemon dir");
std::fs::write(&socket, b"").expect("leave a stale socket file");
let cap = DestructiveIndexDelete::acquire().expect("a stale socket still resolves");
let outcome = cap.delete("my-index").await;
assert!(
matches!(outcome, DeleteOutcome::Transport(_)),
"an unanswered delete must report the transport failure, never a removal: {outcome:?}"
);
}
#[serial_test::serial]
#[tokio::test]
async fn a_refusal_is_never_reported_as_removed() {
let (_dir, _scope) = isolated_daemon_home(true);
let _daemon = uds_mock::spawn_at(
resolved_socket(),
refusing(
crate::daemon::error::CODE_NOT_FOUND,
"unknown index: my-index",
),
)
.await;
let cap = DestructiveIndexDelete::acquire().expect("capability");
let outcome = cap.delete("my-index").await;
match outcome {
DeleteOutcome::Refused { code, ref message } => {
assert_eq!(code, crate::daemon::error::CODE_NOT_FOUND);
assert!(message.contains("unknown index"), "message: {message}");
}
other => panic!("a refusal must carry the daemon's own code: {other:?}"),
}
}
#[serial_test::serial]
#[tokio::test]
async fn a_result_that_did_not_remove_is_not_reported_as_removed() {
let (_dir, _scope) = isolated_daemon_home(true);
let _daemon = uds_mock::spawn_at(
resolved_socket(),
uds_mock::always(json!({ "ok": true, "removed": false, "quiesced": false })),
)
.await;
let cap = DestructiveIndexDelete::acquire().expect("capability");
let outcome = cap.delete("my-index").await;
match outcome {
DeleteOutcome::NotRemoved(ref detail) => {
assert!(detail.contains("quiesced: false"), "detail: {detail}");
}
other => panic!("an abandoned delete must not read as a removal: {other:?}"),
}
}
#[serial_test::serial]
#[tokio::test]
async fn a_result_reporting_removal_is_removed() {
let (_dir, _scope) = isolated_daemon_home(true);
let _daemon = uds_mock::spawn_at(
resolved_socket(),
uds_mock::always(json!({ "ok": true, "removed": true, "data_deleted": true })),
)
.await;
let cap = DestructiveIndexDelete::acquire().expect("capability");
let outcome = cap.delete("my-index").await;
assert!(
matches!(outcome, DeleteOutcome::Removed),
"a confirmed removal must read as one: {outcome:?}"
);
}