use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use crate::error::Error;
#[derive(Clone, Default)]
pub struct ExecutionContext {
pub tenant_id: Option<String>,
pub user_id: Option<String>,
pub workspace: Option<PathBuf>,
pub credentials: Option<Arc<dyn CredentialResolver>>,
pub audit_sink: Option<Arc<dyn AuditSink>>,
}
impl std::fmt::Debug for ExecutionContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExecutionContext")
.field("tenant_id", &self.tenant_id)
.field("user_id", &self.user_id)
.field("workspace", &self.workspace)
.field(
"credentials",
&self.credentials.as_ref().map(|_| "<resolver>"),
)
.field("audit_sink", &self.audit_sink.as_ref().map(|_| "<sink>"))
.finish()
}
}
pub trait CredentialResolver: Send + Sync {
fn resolve(
&self,
name: &str,
) -> Pin<Box<dyn Future<Output = Result<Secret, Error>> + Send + '_>>;
}
pub trait AuditSink: Send + Sync {
fn record(
&self,
record: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>>;
}
#[derive(Clone)]
pub struct Secret(String);
impl Secret {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Secret(<redacted>)")
}
}
impl std::fmt::Display for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<redacted>")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execution_context_default_has_no_identity() {
let ctx = ExecutionContext::default();
assert!(ctx.tenant_id.is_none());
assert!(ctx.user_id.is_none());
assert!(ctx.workspace.is_none());
assert!(ctx.credentials.is_none());
assert!(ctx.audit_sink.is_none());
}
#[test]
fn execution_context_clone_preserves_fields() {
let ctx = ExecutionContext {
tenant_id: Some("tenant-1".into()),
user_id: Some("user-2".into()),
workspace: Some(PathBuf::from("/tmp/ws")),
credentials: None,
audit_sink: None,
};
let cloned = ctx.clone();
assert_eq!(cloned.tenant_id.as_deref(), Some("tenant-1"));
assert_eq!(cloned.user_id.as_deref(), Some("user-2"));
assert_eq!(cloned.workspace, Some(PathBuf::from("/tmp/ws")));
}
#[test]
fn secret_debug_redacts() {
let s = Secret::new("super-secret-token");
let debug = format!("{:?}", s);
assert!(!debug.contains("super-secret-token"));
assert!(debug.contains("<redacted>"));
}
#[test]
fn secret_display_redacts() {
let s = Secret::new("super-secret-token");
let display = format!("{}", s);
assert!(!display.contains("super-secret-token"));
assert!(display.contains("<redacted>"));
}
#[test]
fn secret_expose_returns_inner() {
let s = Secret::new("super-secret-token");
assert_eq!(s.expose(), "super-secret-token");
}
#[test]
fn execution_context_debug_does_not_leak_resolver_internals() {
struct DummyResolver;
impl CredentialResolver for DummyResolver {
fn resolve(
&self,
_name: &str,
) -> Pin<Box<dyn Future<Output = Result<Secret, Error>> + Send + '_>> {
Box::pin(async { Ok(Secret::new("x")) })
}
}
let ctx = ExecutionContext {
credentials: Some(Arc::new(DummyResolver)),
..ExecutionContext::default()
};
let debug = format!("{:?}", ctx);
assert!(debug.contains("<resolver>"));
assert!(!debug.contains("DummyResolver"));
}
}