use std::sync::{Arc, Mutex};
use everscribe::event::{self, Event, Outcome, OutcomeCapture};
use everscribe::recorder::{RecordError, Recorder};
#[derive(Clone, Default)]
struct FakeRecorder(Arc<Mutex<Vec<Event>>>);
impl FakeRecorder {
fn events(&self) -> Vec<Event> {
self.0.lock().unwrap().clone()
}
}
impl Recorder for FakeRecorder {
async fn record(&self, e: Event) -> Result<(), RecordError> {
self.0.lock().unwrap().push(e);
Ok(())
}
}
struct NeverCapture;
impl OutcomeCapture for NeverCapture {
fn outcome(&self) -> Option<Outcome> {
None
}
}
#[tokio::test]
async fn idempotency_key_stamped_on_current_not_template() {
let tmpl = Event::new("");
let ((current_key, current_id, clone_key, clone_id), _handle) =
event::scope(tmpl, None, async {
let current = event::current().snapshot();
let clone = event::new_from_context();
(
current.idempotency_key.clone(),
current.id.clone(),
clone.idempotency_key.clone(),
clone.id.clone(),
)
})
.await;
assert!(
!current_key.is_empty(),
"scope must stamp an idempotency key on the current event"
);
assert_eq!(
current_key, current_id,
"the stamped idempotency key must equal the current event's own id"
);
assert!(
clone_key.is_empty(),
"new_from_context must not inherit the idempotency key from the template"
);
assert_ne!(
clone_id, current_id,
"new_from_context must return a distinct event, not the current one"
);
}
#[tokio::test]
async fn handler_supplied_idempotency_key_overrides_default() {
let tmpl = Event::new("");
let (custom_key, _handle) = event::scope(tmpl, None, async {
event::current().with(|e| e.idempotency_key = "caller-supplied".to_string());
event::current().snapshot().idempotency_key
})
.await;
assert_eq!(custom_key, "caller-supplied");
}
#[tokio::test]
async fn new_from_context_clones_are_distinct_and_keyless() {
let mut tmpl = Event::new("");
tmpl.actor.r#type = "user".to_string();
tmpl.action = "seed.action".to_string();
let ((a_id, a_key, a_action), (b_id, b_key, b_action)) =
event::scope(tmpl, None, async {
let a = event::new_from_context();
let b = event::new_from_context();
(
(a.id.clone(), a.idempotency_key.clone(), a.action.clone()),
(b.id.clone(), b.idempotency_key.clone(), b.action.clone()),
)
})
.await
.0;
assert_ne!(a_id, b_id, "each clone must get its own fresh id");
assert!(a_key.is_empty(), "clones must never carry an idempotency key");
assert!(b_key.is_empty(), "clones must never carry an idempotency key");
assert_eq!(a_action, "seed.action");
assert_eq!(b_action, "seed.action");
}
#[tokio::test]
async fn new_from_context_outside_scope_is_a_minimal_event() {
let e = event::new_from_context();
assert!(e.action.is_empty());
assert!(e.idempotency_key.is_empty());
}
#[tokio::test]
async fn end_alone_records_once_when_handler_only_sets_action() {
let rec = FakeRecorder::default();
let tmpl = Event::new("");
let (_, handle) = event::scope(tmpl, None, async {
event::current().with(|e| e.action = "user.login".to_string());
})
.await;
event::end(&handle, None, Some(&rec)).await;
event::end(&handle, None, Some(&rec)).await;
assert_eq!(rec.events().len(), 1);
assert!(handle.is_recorded());
}
#[tokio::test]
async fn manual_record_then_auto_end_records_only_once() {
let rec = FakeRecorder::default();
let tmpl = Event::new("");
let (_, handle) = event::scope(tmpl, None, async {
event::current().with(|e| e.action = "user.login".to_string());
let ready = event::current()
.prepare_for_record(None)
.expect("first claim on a fresh event must win");
rec.record(ready).await.unwrap();
})
.await;
event::end(&handle, None, Some(&rec)).await;
let events = rec.events();
assert_eq!(
events.len(),
1,
"a handler that sets action and records manually must not also get an auto-recorded duplicate"
);
assert!(handle.is_recorded());
}
#[tokio::test]
async fn second_manual_claim_is_refused() {
let tmpl = Event::new("");
let ((first, second), _handle) = event::scope(tmpl, None, async {
event::current().with(|e| e.action = "user.login".to_string());
let first = event::current().prepare_for_record(None);
let second = event::current().prepare_for_record(None);
(first, second)
})
.await;
assert!(first.is_some(), "the first claim must win");
assert!(second.is_none(), "a second claim on the same event must be refused");
}
#[tokio::test]
async fn prepare_event_never_stamps_sentinel_mid_handler() {
let capture: Arc<dyn OutcomeCapture> = Arc::new(NeverCapture);
let tmpl = Event::new("");
let (mid_handler_outcome_is_empty, handle) = event::scope(tmpl, Some(capture), async {
event::current().with(|e| e.action = "user.login".to_string());
let mut extra = event::new_from_context();
event::prepare_event(&mut extra);
extra.outcome.is_empty()
})
.await;
assert!(
mid_handler_outcome_is_empty,
"prepare_event (non-final) must not apply the \"no response written\" sentinel \
just because the capture has not produced an outcome yet"
);
let rec = FakeRecorder::default();
let final_capture: Arc<dyn OutcomeCapture> = Arc::new(NeverCapture);
event::end(&handle, Some(final_capture.as_ref()), Some(&rec)).await;
let events = rec.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].outcome.status, "error");
assert_eq!(events[0].outcome.code, 0);
assert_eq!(
events[0].outcome.message,
Some(serde_json::json!("no response written"))
);
}
#[tokio::test]
async fn prepare_for_record_never_stamps_sentinel_when_not_final() {
let capture: Arc<dyn OutcomeCapture> = Arc::new(NeverCapture);
let tmpl = Event::new("");
let (ready, _handle) = event::scope(tmpl, Some(capture.clone()), async move {
event::current().with(|e| e.action = "user.login".to_string());
event::current().prepare_for_record(Some(capture.as_ref()))
})
.await;
let ready = ready.expect("first claim must win");
assert!(
ready.outcome.is_empty(),
"prepare_for_record (non-final) must not apply the sentinel"
);
}
#[tokio::test]
async fn explicit_outcome_set_by_handler_is_never_overwritten() {
let capture: Arc<dyn OutcomeCapture> = Arc::new(NeverCapture);
let rec = FakeRecorder::default();
let tmpl = Event::new("");
let (_, handle) = event::scope(tmpl, Some(capture.clone()), async {
event::current().with(|e| {
e.action = "password.reset".to_string();
e.outcome = Outcome {
status: "denied".to_string(),
code: 418,
message: None,
};
});
})
.await;
event::end(&handle, Some(capture.as_ref()), Some(&rec)).await;
let events = rec.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].outcome.status, "denied");
assert_eq!(events[0].outcome.code, 418);
}