use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use agentstategraph::Repository;
use agentstategraph_mcp::auth::{ApiKey, KeyRef, TenantManager};
use agentstategraph_mcp::http;
use agentstategraph_storage::SqliteStorage;
#[derive(Clone, Default)]
struct Captured(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Captured {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
const KEY: &str = "asg_request_log_secret_key_00000000000";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_logs_carry_the_key_id_never_the_key() {
let captured = Captured::default();
let _telemetry = oxtel::builder()
.without_env_exporters()
.with_local_logs(oxtel::sdk::logs::LocalFormat::Json)
.with_local_logs_writer(captured.clone())
.install()
.expect("install telemetry");
let repo = Arc::new(Repository::new(Box::new(
SqliteStorage::in_memory().expect("in-memory sqlite"),
)));
repo.init().unwrap();
let mgr = TenantManager::multi_tenant(Arc::clone(&repo), None).unwrap();
mgr.register_key(ApiKey {
id: String::new(),
key: KEY.into(),
tenant_id: "default".into(),
name: "test".into(),
plan: "free".into(),
enabled: true,
created_at: chrono::Utc::now().to_rfc3339(),
namespace_id: None,
commit_agent_id: None,
can_migrate: false,
is_admin: false,
expires_at: None,
last_used_at: None,
})
.unwrap();
let id = mgr.key_id(KeyRef::Key(KEY)).unwrap();
let app = http::with_observability(http::build_router_with(Arc::clone(&mgr), 0, None));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr: SocketAddr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
});
let status = reqwest::Client::new()
.get(format!("http://{addr}/api/state/main?path=/&secret=query"))
.bearer_auth(KEY)
.send()
.await
.unwrap()
.status();
assert_eq!(status.as_u16(), 200);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let log = String::from_utf8(captured.0.lock().unwrap().clone()).unwrap();
let line: serde_json::Value = log
.lines()
.filter_map(|l| serde_json::from_str(l).ok())
.find(|l: &serde_json::Value| l["message"] == "request")
.unwrap_or_else(|| panic!("no request line in:\n{log}"));
assert_eq!(line["http.request.method"], "GET", "{line}");
assert_eq!(line["url.path"], "/api/state/main", "{line}");
assert_eq!(line["http.route"], "/api/state/{ref_name}", "{line}");
assert_eq!(line["http.response.status_code"], 200, "{line}");
assert!(line["http.server.request.duration"].is_f64(), "{line}");
assert_eq!(line["asg.api_key.id"], id.as_str(), "{line}");
assert!(!log.contains(KEY), "the key reached the log:\n{log}");
assert!(
!log.contains("secret=query"),
"the query reached the log:\n{log}"
);
}