#![cfg(feature = "pg-tests")]
use std::time::Duration;
use auditlog::{Action, Auditable, AuditId, AuditOptions, SqlxBackend, ValueMap};
use serde_json::json;
use testcontainers_modules::postgres::Postgres;
use testcontainers_modules::testcontainers::ContainerAsync;
use testcontainers_modules::testcontainers::runners::AsyncRunner;
#[derive(Clone)]
struct Doc {
id: i64,
title: String,
status: i64,
}
impl Auditable for Doc {
fn auditable_type() -> &'static str {
"Doc"
}
fn auditable_id(&self) -> AuditId {
self.id.into()
}
fn audited_attributes(&self) -> ValueMap {
let mut m = ValueMap::new();
m.insert("id".into(), json!(self.id));
m.insert("title".into(), json!(self.title));
m.insert("status".into(), json!(self.status));
m
}
fn audit_options() -> AuditOptions {
AuditOptions::default()
}
}
#[derive(Clone)]
struct Capped {
id: i64,
title: String,
status: i64,
}
impl Auditable for Capped {
fn auditable_type() -> &'static str {
"Capped"
}
fn auditable_id(&self) -> AuditId {
self.id.into()
}
fn audited_attributes(&self) -> ValueMap {
let mut m = ValueMap::new();
m.insert("id".into(), json!(self.id));
m.insert("title".into(), json!(self.title));
m.insert("status".into(), json!(self.status));
m
}
fn audit_options() -> AuditOptions {
AuditOptions::builder().max_audits(2).build()
}
}
struct Company {
id: i64,
name: String,
}
impl Auditable for Company {
fn auditable_type() -> &'static str {
"Company"
}
fn auditable_id(&self) -> AuditId {
self.id.into()
}
fn audited_attributes(&self) -> ValueMap {
let mut m = ValueMap::new();
m.insert("id".into(), json!(self.id));
m.insert("name".into(), json!(self.name));
m
}
fn audit_options() -> AuditOptions {
AuditOptions::default()
}
}
struct Employee {
id: i64,
name: String,
company_id: i64,
}
impl Auditable for Employee {
fn auditable_type() -> &'static str {
"Employee"
}
fn auditable_id(&self) -> AuditId {
self.id.into()
}
fn audited_attributes(&self) -> ValueMap {
let mut m = ValueMap::new();
m.insert("id".into(), json!(self.id));
m.insert("name".into(), json!(self.name));
m.insert("company_id".into(), json!(self.company_id));
m
}
fn audit_options() -> AuditOptions {
AuditOptions::builder().associated_with("Company").build()
}
fn audit_associated(&self) -> Option<(String, AuditId)> {
Some(("Company".to_string(), self.company_id.into()))
}
}
async fn start_pg() -> Option<(ContainerAsync<Postgres>, SqlxBackend)> {
let container = match Postgres::default().start().await {
Ok(c) => c,
Err(e) => {
eprintln!("SKIP postgres integration tests: could not start container (is Docker running?): {e}");
return None;
}
};
let host = container.get_host().await.expect("container host");
let port = container
.get_host_port_ipv4(5432)
.await
.expect("container port");
let url = format!("postgres://postgres:postgres@{host}:{port}/postgres");
let mut backend = None;
for _ in 0..40 {
match SqlxBackend::connect_postgres(&url).await {
Ok(b) => {
backend = Some(b);
break;
}
Err(_) => tokio::time::sleep(Duration::from_millis(250)).await,
}
}
let backend = backend.expect("connect to postgres container");
backend.migrate().await.expect("migrate");
Some((container, backend))
}
#[tokio::test]
async fn postgres_end_to_end() {
let Some((_container, b)) = start_pg().await else {
return; };
let mut doc = Doc {
id: 1,
title: "Hello".into(),
status: 0,
};
let create = doc.audited_create(&b).await.unwrap().unwrap();
assert_eq!(create.action, Action::Create);
assert_eq!(create.version, 1);
assert_eq!(create.audited_changes.0.get("title"), Some(&json!("Hello")));
assert!(create.request_uuid.as_deref().is_some_and(|s| !s.is_empty()));
let old = doc.clone();
doc.title = "Hello, world".into();
let update = doc.audited_update(&b, &old).await.unwrap().unwrap();
assert_eq!(update.version, 2);
assert_eq!(
update.audited_changes.0.get("title"),
Some(&json!(["Hello", "Hello, world"]))
);
let destroy = doc.audited_destroy(&b).await.unwrap().unwrap();
assert_eq!(destroy.version, 3);
assert_eq!(destroy.audited_changes.0.get("title"), Some(&json!("Hello, world")));
let audits = Doc::audits(&b, 1).await.unwrap();
assert_eq!(audits.iter().map(|a| a.version).collect::<Vec<_>>(), vec![1, 2, 3]);
assert_eq!(Doc::query(&b, 1).updates().count().await.unwrap(), 1);
assert_eq!(Doc::query(&b, 1).creates().count().await.unwrap(), 1);
assert_eq!(Doc::query(&b, 1).destroys().count().await.unwrap(), 1);
let desc = Doc::query(&b, 1).descending().limit(1).fetch().await.unwrap();
assert_eq!(desc[0].version, 3);
let from2 = Doc::query(&b, 1).from_version(2).fetch().await.unwrap();
assert_eq!(from2.iter().map(|a| a.version).collect::<Vec<_>>(), vec![2, 3]);
let v1 = Doc::revision(&b, 1, 1).await.unwrap().unwrap();
assert_eq!(v1.attributes.get("title"), Some(&json!("Hello")));
let now = chrono::Utc::now();
let at_now = Doc::revision_at(&b, 1, now).await.unwrap();
assert!(at_now.is_some());
let before = Doc::revision_at(&b, 1, now - chrono::Duration::days(1))
.await
.unwrap();
assert!(before.is_none());
let mut c = Capped {
id: 1,
title: "Foobar".into(),
status: 0,
};
c.audited_create(&b).await.unwrap(); let o1 = c.clone();
c.title = "Awesome".into();
c.audited_update_with_comment(&b, &o1, "first audit comment")
.await
.unwrap(); let o2 = c.clone();
c.status = 7;
c.audited_update_with_comment(&b, &o2, "second audit comment")
.await
.unwrap(); let capped = Capped::audits(&b, 1).await.unwrap();
assert_eq!(capped.len(), 2);
assert_eq!(capped.iter().map(|a| a.version).collect::<Vec<_>>(), vec![2, 3]);
assert_eq!(
capped[0].audited_changes.0.get("title"),
Some(&json!(["Foobar", "Awesome"]))
);
let comment = capped[0].comment.as_deref().unwrap_or("");
assert!(comment.contains("first audit comment"));
assert!(comment.contains("result of multiple"));
let company = Company {
id: 10,
name: "Acme".into(),
};
company.audited_create(&b).await.unwrap();
let emp = Employee {
id: 1,
name: "Alice".into(),
company_id: 10,
};
let emp_audit = emp.audited_create(&b).await.unwrap().unwrap();
assert_eq!(emp_audit.associated_type.as_deref(), Some("Company"));
assert_eq!(emp_audit.associated_id.as_ref().map(|i| i.as_str()), Some("10"));
assert_eq!(Company::associated_audits(&b, 10).await.unwrap().len(), 1);
assert_eq!(
Company::own_and_associated_audits(&b, 10).await.unwrap().len(),
2
);
}