use crate::Cirrus;
use crate::error::CirrusResult;
const CSV_ACCEPT: &str = "text/csv";
impl Cirrus {
pub fn event_monitoring(&self) -> EventMonitoringHandler<'_> {
EventMonitoringHandler { client: self }
}
}
#[derive(Debug)]
pub struct EventMonitoringHandler<'a> {
client: &'a Cirrus,
}
impl EventMonitoringHandler<'_> {
pub async fn download(&self, log_file_id: &str) -> CirrusResult<bytes::Bytes> {
let path = format!("sobjects/EventLogFile/{log_file_id}/LogFile");
let (_headers, bytes) = self
.client
.fetch_raw(reqwest::Method::GET, &path, CSV_ACCEPT, None)
.await?;
Ok(bytes)
}
pub async fn download_url(&self, log_file_url: &str) -> CirrusResult<bytes::Bytes> {
let path = if log_file_url.starts_with('/') || log_file_url.starts_with("http") {
log_file_url.to_string()
} else {
format!("/{log_file_url}")
};
let (_headers, bytes) = self
.client
.fetch_raw(reqwest::Method::GET, &path, CSV_ACCEPT, None)
.await?;
Ok(bytes)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::auth::StaticTokenAuth;
use std::sync::Arc;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn fixture(uri: String) -> Cirrus {
let auth = Arc::new(StaticTokenAuth::new("tok", uri));
Cirrus::builder().auth(auth).build().unwrap()
}
#[tokio::test]
async fn download_targets_logfile_endpoint_and_returns_csv_bytes() {
let server = MockServer::start().await;
let csv = "TIMESTAMP,EVENT_TYPE,USER_ID\n2024-01-01T00:00:00.000Z,API,005xx\n";
Mock::given(method("GET"))
.and(path(
"/services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ/LogFile",
))
.and(header("authorization", "Bearer tok"))
.and(header("accept", "text/csv"))
.respond_with(ResponseTemplate::new(200).set_body_string(csv))
.mount(&server)
.await;
let sf = fixture(server.uri());
let bytes = sf
.event_monitoring()
.download("0ATD000000001bROAQ")
.await
.unwrap();
assert_eq!(&bytes[..], csv.as_bytes());
}
#[tokio::test]
async fn download_url_accepts_instance_relative_path_from_query() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(
"/services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ/LogFile",
))
.respond_with(ResponseTemplate::new(200).set_body_string("X,Y\n1,2\n"))
.mount(&server)
.await;
let sf = fixture(server.uri());
let bytes = sf
.event_monitoring()
.download_url("/services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ/LogFile")
.await
.unwrap();
assert_eq!(&bytes[..], b"X,Y\n1,2\n");
}
#[tokio::test]
async fn download_url_normalizes_path_without_leading_slash() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(
"/services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ/LogFile",
))
.respond_with(ResponseTemplate::new(200).set_body_string("ok\n"))
.mount(&server)
.await;
let sf = fixture(server.uri());
let bytes = sf
.event_monitoring()
.download_url("services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ/LogFile")
.await
.unwrap();
assert_eq!(&bytes[..], b"ok\n");
}
#[tokio::test]
async fn download_surfaces_404_as_api_error() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(
"/services/data/v66.0/sobjects/EventLogFile/0ATmissing/LogFile",
))
.respond_with(
ResponseTemplate::new(404).set_body_json(serde_json::json!([{
"errorCode": "NOT_FOUND",
"message": "The requested resource does not exist"
}])),
)
.mount(&server)
.await;
let sf = fixture(server.uri());
let err = sf
.event_monitoring()
.download("0ATmissing")
.await
.unwrap_err();
match err {
crate::CirrusError::Api { status, errors, .. } => {
assert_eq!(status, 404);
assert_eq!(errors[0].error_code, "NOT_FOUND");
}
other => panic!("expected Api error, got {other:?}"),
}
}
#[tokio::test]
async fn query_via_existing_method_deserializes_into_event_log_file_record() {
use crate::EventLogFileRecord;
use crate::QueryResult;
use wiremock::matchers::query_param;
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/services/data/v66.0/query"))
.and(query_param(
"q",
"SELECT Id, EventType, LogFile, LogDate, LogFileLength FROM EventLogFile",
))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"totalSize": 1,
"done": true,
"records": [{
"attributes": {
"type": "EventLogFile",
"url": "/services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ"
},
"Id": "0ATD000000001bROAQ",
"EventType": "API",
"LogFile": "/services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ/LogFile",
"LogDate": "2014-03-14T00:00:00.000+0000",
"LogFileLength": 2692.0
}]
})))
.mount(&server)
.await;
let sf = fixture(server.uri());
let result: QueryResult<EventLogFileRecord> = sf
.query_as("SELECT Id, EventType, LogFile, LogDate, LogFileLength FROM EventLogFile")
.await
.unwrap();
let rec = &result.records[0];
assert_eq!(rec.id, "0ATD000000001bROAQ");
assert_eq!(rec.event_type, "API");
assert_eq!(
rec.log_file,
"/services/data/v66.0/sobjects/EventLogFile/0ATD000000001bROAQ/LogFile"
);
assert_eq!(rec.log_file_length, Some(2692.0));
assert!(rec.interval.is_none());
assert!(rec.sequence.is_none());
}
#[tokio::test]
async fn query_with_hourly_fields_populates_interval_and_sequence() {
use crate::EventLogFileRecord;
use crate::QueryResult;
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/services/data/v66.0/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"totalSize": 1,
"done": true,
"records": [{
"attributes": {"type": "EventLogFile"},
"Id": "0ATxx",
"EventType": "URI",
"LogFile": "/services/data/v66.0/sobjects/EventLogFile/0ATxx/LogFile",
"LogDate": "2024-03-14T00:00:00.000+0000",
"Interval": "Hourly",
"Sequence": 3,
"CreatedDate": "2024-03-14T03:30:00.000+0000"
}]
})))
.mount(&server)
.await;
let sf = fixture(server.uri());
let result: QueryResult<EventLogFileRecord> = sf
.query_as(
"SELECT Id, EventType, LogFile, LogDate, Interval, Sequence, CreatedDate \
FROM EventLogFile WHERE Interval = 'Hourly'",
)
.await
.unwrap();
let rec = &result.records[0];
assert_eq!(rec.interval.as_deref(), Some("Hourly"));
assert_eq!(rec.sequence, Some(3));
assert!(rec.created_date.is_some());
}
}