use std::future::Future;
use std::pin::Pin;
use chrono::{DateTime, Utc};
use serde::Serialize;
use serde_json::Value;
use uuid::Uuid;
use crate::types::UserId;
#[derive(Debug, Clone, Serialize)]
pub struct AuthEvent {
pub event_id: Uuid,
pub event_type: String,
pub user_id: Option<UserId>,
pub timestamp: DateTime<Utc>,
pub data: Value,
}
impl AuthEvent {
pub fn new(event_type: impl Into<String>, user_id: Option<UserId>, data: Value) -> Self {
Self {
event_id: Uuid::now_v7(),
event_type: event_type.into(),
user_id,
timestamp: Utc::now(),
data,
}
}
}
pub trait EventSink: Send + Sync {
fn emit<'a>(&'a self, event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
}
pub struct NoopEventSink;
impl EventSink for NoopEventSink {
fn emit<'a>(&'a self, _event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(std::future::ready(()))
}
}
pub struct LoggingEventSink;
impl EventSink for LoggingEventSink {
fn emit<'a>(&'a self, event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
tracing::debug!(
event_type = %event.event_type,
user_id = ?event.user_id,
data = %event.data,
"auth event",
);
Box::pin(std::future::ready(()))
}
}
impl<T: EventSink + ?Sized> EventSink for std::sync::Arc<T> {
fn emit<'a>(&'a self, event: &'a AuthEvent) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
(**self).emit(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn _assert_object_safe(_: &dyn EventSink) {}
fn sample_event() -> AuthEvent {
AuthEvent::new("user.created", None, serde_json::json!({}))
}
#[tokio::test]
async fn noop_sink_returns_immediately() {
NoopEventSink.emit(&sample_event()).await;
}
#[tokio::test]
async fn logging_sink_returns_immediately() {
LoggingEventSink.emit(&sample_event()).await;
}
#[tokio::test]
async fn arc_dispatch_works() {
let sink: std::sync::Arc<dyn EventSink> = std::sync::Arc::new(NoopEventSink);
sink.emit(&sample_event()).await;
}
#[tokio::test]
async fn auth_event_new_stamps_timestamp() {
let before = Utc::now();
let event = AuthEvent::new("test", None, serde_json::json!({"k": "v"}));
let after = Utc::now();
assert!(event.timestamp >= before);
assert!(event.timestamp <= after);
assert_eq!(event.event_type, "test");
assert!(event.user_id.is_none());
}
#[tokio::test]
async fn auth_event_new_assigns_distinct_non_nil_event_ids() {
let a = AuthEvent::new("test", None, serde_json::json!({}));
let b = AuthEvent::new("test", None, serde_json::json!({}));
assert_ne!(a.event_id, Uuid::nil());
assert_ne!(b.event_id, Uuid::nil());
assert_ne!(a.event_id, b.event_id);
}
#[tokio::test]
async fn auth_event_serializes_event_id_field() {
let event = AuthEvent::new("test", None, serde_json::json!({}));
let json = serde_json::to_value(&event).unwrap();
assert_eq!(
json["event_id"].as_str().unwrap(),
event.event_id.to_string()
);
}
}