use super::schema::MANDATE_SCHEMA;
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection, OptionalExtension};
use sha2::{Digest, Sha256};
use std::path::Path;
use std::sync::{Arc, Mutex};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthzReceipt {
pub mandate_id: String,
pub use_id: String,
pub use_count: u32,
pub consumed_at: DateTime<Utc>,
pub tool_call_id: String,
pub was_new: bool,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum AuthzError {
#[error("Mandate not found: {mandate_id}")]
MandateNotFound { mandate_id: String },
#[error("Mandate already used (single_use=true)")]
AlreadyUsed,
#[error("Max uses exceeded: {current} > {max}")]
MaxUsesExceeded { max: u32, current: u32 },
#[error("Nonce replay detected: {nonce}")]
NonceReplay { nonce: String },
#[error("Mandate metadata conflict for {mandate_id}: stored {field} differs")]
MandateConflict { mandate_id: String, field: String },
#[error("Invalid mandate constraints: single_use=true with max_uses={max_uses}")]
InvalidConstraints { max_uses: u32 },
#[error("Mandate revoked at {revoked_at}")]
Revoked { revoked_at: DateTime<Utc> },
#[error("Database error: {0}")]
Database(String),
}
impl From<rusqlite::Error> for AuthzError {
fn from(e: rusqlite::Error) -> Self {
AuthzError::Database(e.to_string())
}
}
#[derive(Debug, Clone)]
pub struct MandateMetadata {
pub mandate_id: String,
pub mandate_kind: String,
pub audience: String,
pub issuer: String,
pub expires_at: Option<DateTime<Utc>>,
pub single_use: bool,
pub max_uses: Option<u32>,
pub canonical_digest: String,
pub key_id: String,
}
#[derive(Debug, Clone)]
pub struct ConsumeParams<'a> {
pub mandate_id: &'a str,
pub tool_call_id: &'a str,
pub nonce: Option<&'a str>,
pub audience: &'a str,
pub issuer: &'a str,
pub tool_name: &'a str,
pub operation_class: &'a str,
pub source_run_id: Option<&'a str>,
}
#[derive(Clone)]
pub struct MandateStore {
conn: Arc<Mutex<Connection>>,
}
impl MandateStore {
pub fn open(path: &Path) -> Result<Self, AuthzError> {
let conn = Connection::open(path)?;
Self::init_connection(&conn)?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
pub fn memory() -> Result<Self, AuthzError> {
let conn = Connection::open_in_memory()?;
Self::init_connection(&conn)?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
pub fn from_connection(conn: Connection) -> Result<Self, AuthzError> {
Self::init_connection(&conn)?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
fn init_connection(conn: &Connection) -> Result<(), AuthzError> {
conn.execute("PRAGMA foreign_keys = ON", [])?;
let _ = conn.execute("PRAGMA journal_mode = WAL", []);
let _ = conn.execute("PRAGMA busy_timeout = 5000", []);
conn.execute_batch(MANDATE_SCHEMA)?;
Ok(())
}
pub fn upsert_mandate(&self, meta: &MandateMetadata) -> Result<(), AuthzError> {
if meta.single_use {
if let Some(max) = meta.max_uses {
if max != 1 {
return Err(AuthzError::InvalidConstraints { max_uses: max });
}
}
}
let conn = self.conn.lock().unwrap();
conn.execute(
r#"
INSERT INTO mandates (
mandate_id, mandate_kind, audience, issuer, expires_at,
single_use, max_uses, use_count, canonical_digest, key_id
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 0, ?8, ?9)
ON CONFLICT(mandate_id) DO NOTHING
"#,
params![
meta.mandate_id,
meta.mandate_kind,
meta.audience,
meta.issuer,
meta.expires_at.map(|t| t.to_rfc3339()),
meta.single_use as i32,
meta.max_uses.map(|m| m as i64),
meta.canonical_digest,
meta.key_id,
],
)?;
let stored: Option<(String, String, String, String, String)> = conn
.query_row(
r#"
SELECT mandate_kind, audience, issuer, canonical_digest, key_id
FROM mandates WHERE mandate_id = ?
"#,
[&meta.mandate_id],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.optional()?;
if let Some((kind, aud, iss, digest, key)) = stored {
if kind != meta.mandate_kind {
return Err(AuthzError::MandateConflict {
mandate_id: meta.mandate_id.clone(),
field: "mandate_kind".to_string(),
});
}
if aud != meta.audience {
return Err(AuthzError::MandateConflict {
mandate_id: meta.mandate_id.clone(),
field: "audience".to_string(),
});
}
if iss != meta.issuer {
return Err(AuthzError::MandateConflict {
mandate_id: meta.mandate_id.clone(),
field: "issuer".to_string(),
});
}
if digest != meta.canonical_digest {
return Err(AuthzError::MandateConflict {
mandate_id: meta.mandate_id.clone(),
field: "canonical_digest".to_string(),
});
}
if key != meta.key_id {
return Err(AuthzError::MandateConflict {
mandate_id: meta.mandate_id.clone(),
field: "key_id".to_string(),
});
}
}
Ok(())
}
pub fn consume_mandate(&self, params: &ConsumeParams<'_>) -> Result<AuthzReceipt, AuthzError> {
let conn = self.conn.lock().unwrap();
conn.execute("BEGIN IMMEDIATE", [])?;
let result = self.consume_mandate_inner(&conn, params);
match &result {
Ok(_) => {
conn.execute("COMMIT", [])?;
}
Err(_) => {
let _ = conn.execute("ROLLBACK", []);
}
}
result
}
fn consume_mandate_inner(
&self,
conn: &Connection,
params: &ConsumeParams<'_>,
) -> Result<AuthzReceipt, AuthzError> {
let ConsumeParams {
mandate_id,
tool_call_id,
nonce,
audience,
issuer,
tool_name,
operation_class,
source_run_id,
} = params;
let existing: Option<(String, i64, String)> = conn
.query_row(
"SELECT use_id, use_count, consumed_at FROM mandate_uses WHERE tool_call_id = ?",
[tool_call_id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.optional()?;
if let Some((use_id, use_count, consumed_at)) = existing {
return Ok(AuthzReceipt {
mandate_id: mandate_id.to_string(),
use_id,
use_count: use_count as u32,
consumed_at: DateTime::parse_from_rfc3339(&consumed_at)
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(|_| Utc::now()),
tool_call_id: tool_call_id.to_string(),
was_new: false, });
}
if let Some(n) = nonce {
let insert_result = conn.execute(
"INSERT INTO nonces (audience, issuer, nonce, mandate_id) VALUES (?1, ?2, ?3, ?4)",
params![audience, issuer, n, mandate_id],
);
if let Err(e) = insert_result {
if e.to_string().contains("UNIQUE constraint failed") {
return Err(AuthzError::NonceReplay {
nonce: n.to_string(),
});
}
return Err(e.into());
}
}
let row: Option<(i64, i32, Option<i64>)> = conn
.query_row(
"SELECT use_count, single_use, max_uses FROM mandates WHERE mandate_id = ?",
[mandate_id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.optional()?;
let (current_count, single_use, max_uses) = match row {
Some(r) => r,
None => {
return Err(AuthzError::MandateNotFound {
mandate_id: mandate_id.to_string(),
});
}
};
let new_count = current_count + 1;
if single_use != 0 && current_count > 0 {
return Err(AuthzError::AlreadyUsed);
}
if let Some(max) = max_uses {
if new_count > max {
return Err(AuthzError::MaxUsesExceeded {
max: max as u32,
current: new_count as u32,
});
}
}
conn.execute(
"UPDATE mandates SET use_count = ?1 WHERE mandate_id = ?2",
params![new_count, mandate_id],
)?;
let use_id = compute_use_id(mandate_id, tool_call_id, new_count as u32);
let consumed_at = Utc::now();
conn.execute(
r#"
INSERT INTO mandate_uses (
use_id, mandate_id, tool_call_id, use_count, consumed_at,
tool_name, operation_class, nonce, source_run_id
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
"#,
params![
use_id,
mandate_id,
tool_call_id,
new_count,
consumed_at.to_rfc3339(),
tool_name,
operation_class,
nonce,
source_run_id,
],
)?;
Ok(AuthzReceipt {
mandate_id: mandate_id.to_string(),
use_id,
use_count: new_count as u32,
consumed_at,
tool_call_id: tool_call_id.to_string(),
was_new: true, })
}
pub fn get_use_count(&self, mandate_id: &str) -> Result<Option<u32>, AuthzError> {
let conn = self.conn.lock().unwrap();
let count: Option<i64> = conn
.query_row(
"SELECT use_count FROM mandates WHERE mandate_id = ?",
[mandate_id],
|row| row.get(0),
)
.optional()?;
Ok(count.map(|c| c as u32))
}
pub fn count_uses(&self, mandate_id: &str) -> Result<u32, AuthzError> {
let conn = self.conn.lock().unwrap();
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM mandate_uses WHERE mandate_id = ?",
[mandate_id],
|row| row.get(0),
)?;
Ok(count as u32)
}
pub fn nonce_exists(
&self,
audience: &str,
issuer: &str,
nonce: &str,
) -> Result<bool, AuthzError> {
let conn = self.conn.lock().unwrap();
let exists: i64 = conn.query_row(
"SELECT COUNT(*) FROM nonces WHERE audience = ? AND issuer = ? AND nonce = ?",
params![audience, issuer, nonce],
|row| row.get(0),
)?;
Ok(exists > 0)
}
pub fn upsert_revocation(&self, r: &RevocationRecord) -> Result<(), AuthzError> {
let conn = self.conn.lock().unwrap();
conn.execute(
r#"
INSERT INTO mandate_revocations (mandate_id, revoked_at, reason, revoked_by, source, event_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(mandate_id) DO UPDATE SET
revoked_at = excluded.revoked_at,
reason = excluded.reason,
revoked_by = excluded.revoked_by,
source = excluded.source,
event_id = excluded.event_id
"#,
params![
r.mandate_id,
r.revoked_at.to_rfc3339(),
r.reason,
r.revoked_by,
r.source,
r.event_id,
],
)?;
Ok(())
}
pub fn get_revoked_at(&self, mandate_id: &str) -> Result<Option<DateTime<Utc>>, AuthzError> {
let conn = self.conn.lock().unwrap();
let s: Option<String> = conn
.query_row(
"SELECT revoked_at FROM mandate_revocations WHERE mandate_id = ?1",
[mandate_id],
|row| row.get(0),
)
.optional()?;
match s {
Some(ts) => {
let dt = DateTime::parse_from_rfc3339(&ts)
.map_err(|e| {
AuthzError::Database(format!("Invalid revoked_at timestamp: {e}"))
})?
.with_timezone(&Utc);
Ok(Some(dt))
}
None => Ok(None),
}
}
pub fn is_revoked(&self, mandate_id: &str) -> Result<bool, AuthzError> {
Ok(self.get_revoked_at(mandate_id)?.is_some())
}
}
#[derive(Debug, Clone)]
pub struct RevocationRecord {
pub mandate_id: String,
pub revoked_at: DateTime<Utc>,
pub reason: Option<String>,
pub revoked_by: Option<String>,
pub source: Option<String>,
pub event_id: Option<String>,
}
pub fn compute_use_id(mandate_id: &str, tool_call_id: &str, use_count: u32) -> String {
let input = format!("{}:{}:{}", mandate_id, tool_call_id, use_count);
let hash = Sha256::digest(input.as_bytes());
format!("sha256:{}", hex::encode(hash))
}
#[cfg(test)]
mod tests {
use super::*;
fn test_metadata() -> MandateMetadata {
MandateMetadata {
mandate_id: "sha256:test123".to_string(),
mandate_kind: "intent".to_string(),
audience: "org/app".to_string(),
issuer: "auth.org.com".to_string(),
expires_at: None,
single_use: false,
max_uses: None,
canonical_digest: "sha256:digest123".to_string(),
key_id: "sha256:key123".to_string(),
}
}
fn consume(
store: &MandateStore,
mandate_id: &str,
tool_call_id: &str,
nonce: Option<&str>,
audience: &str,
issuer: &str,
) -> Result<AuthzReceipt, AuthzError> {
store.consume_mandate(&ConsumeParams {
mandate_id,
tool_call_id,
nonce,
audience,
issuer,
tool_name: "test_tool",
operation_class: "read",
source_run_id: None,
})
}
#[test]
fn test_store_bootstraps_schema() {
let store = MandateStore::memory().unwrap();
let conn = store.conn.lock().unwrap();
let tables: Vec<String> = conn
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.filter_map(|r| r.ok())
.collect();
assert!(tables.contains(&"mandates".to_string()));
assert!(tables.contains(&"mandate_uses".to_string()));
assert!(tables.contains(&"nonces".to_string()));
}
#[test]
fn test_store_sets_foreign_keys() {
let store = MandateStore::memory().unwrap();
let conn = store.conn.lock().unwrap();
let fk: i32 = conn
.query_row("PRAGMA foreign_keys", [], |row| row.get(0))
.unwrap();
assert_eq!(fk, 1);
}
#[test]
fn test_upsert_mandate_inserts_new() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let count = store.get_use_count(&meta.mandate_id).unwrap();
assert_eq!(count, Some(0));
}
#[test]
fn test_upsert_mandate_is_noop_on_same_content() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
store.upsert_mandate(&meta).unwrap();
let count = store.get_use_count(&meta.mandate_id).unwrap();
assert_eq!(count, Some(0));
}
#[test]
fn test_upsert_mandate_rejects_conflicting_metadata() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let mut conflict = meta.clone();
conflict.audience = "different/app".to_string();
let result = store.upsert_mandate(&conflict);
assert!(matches!(
result,
Err(AuthzError::MandateConflict { field, .. }) if field == "audience"
));
}
#[test]
fn test_consume_fails_if_mandate_missing() {
let store = MandateStore::memory().unwrap();
let result = consume(
&store,
"sha256:nonexistent",
"tc_1",
None,
"org/app",
"auth.org.com",
);
assert!(matches!(result, Err(AuthzError::MandateNotFound { .. })));
}
#[test]
fn test_consume_first_time_returns_use_count_1() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let receipt = consume(
&store,
&meta.mandate_id,
"tc_1",
None,
&meta.audience,
&meta.issuer,
)
.unwrap();
assert_eq!(receipt.use_count, 1);
assert!(!receipt.use_id.is_empty());
assert_eq!(receipt.tool_call_id, "tc_1");
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(1));
assert_eq!(store.count_uses(&meta.mandate_id).unwrap(), 1);
}
#[test]
fn test_consume_is_idempotent_for_same_tool_call_id() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let receipt1 = consume(
&store,
&meta.mandate_id,
"tc_1",
None,
&meta.audience,
&meta.issuer,
)
.unwrap();
let receipt2 = consume(
&store,
&meta.mandate_id,
"tc_1",
None,
&meta.audience,
&meta.issuer,
)
.unwrap();
assert_eq!(receipt1.use_id, receipt2.use_id);
assert_eq!(receipt1.use_count, receipt2.use_count);
assert!(receipt1.was_new, "First consume should be was_new=true");
assert!(!receipt2.was_new, "Retry should be was_new=false");
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(1));
assert_eq!(store.count_uses(&meta.mandate_id).unwrap(), 1);
}
#[test]
fn test_consume_increments_for_different_tool_call_ids() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let r1 = consume(
&store,
&meta.mandate_id,
"tc_1",
None,
&meta.audience,
&meta.issuer,
)
.unwrap();
let r2 = consume(
&store,
&meta.mandate_id,
"tc_2",
None,
&meta.audience,
&meta.issuer,
)
.unwrap();
assert_eq!(r1.use_count, 1);
assert_eq!(r2.use_count, 2);
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(2));
assert_eq!(store.count_uses(&meta.mandate_id).unwrap(), 2);
}
#[test]
fn test_single_use_allows_first_then_rejects_second() {
let store = MandateStore::memory().unwrap();
let mut meta = test_metadata();
meta.single_use = true;
meta.max_uses = Some(1);
store.upsert_mandate(&meta).unwrap();
let r1 = consume(
&store,
&meta.mandate_id,
"tc_1",
None,
&meta.audience,
&meta.issuer,
);
assert!(r1.is_ok());
let r2 = consume(
&store,
&meta.mandate_id,
"tc_2",
None,
&meta.audience,
&meta.issuer,
);
assert!(matches!(r2, Err(AuthzError::AlreadyUsed)));
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(1));
}
#[test]
fn test_max_uses_allows_up_to_n_then_rejects() {
let store = MandateStore::memory().unwrap();
let mut meta = test_metadata();
meta.max_uses = Some(3);
store.upsert_mandate(&meta).unwrap();
for i in 1..=3 {
let r = consume(
&store,
&meta.mandate_id,
&format!("tc_{}", i),
None,
&meta.audience,
&meta.issuer,
);
assert!(r.is_ok(), "Call {} should succeed", i);
}
let r4 = consume(
&store,
&meta.mandate_id,
"tc_4",
None,
&meta.audience,
&meta.issuer,
);
assert!(matches!(
r4,
Err(AuthzError::MaxUsesExceeded { max: 3, current: 4 })
));
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(3));
}
#[test]
fn test_max_uses_null_is_unlimited() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata(); store.upsert_mandate(&meta).unwrap();
for i in 1..=20 {
let r = consume(
&store,
&meta.mandate_id,
&format!("tc_{}", i),
None,
&meta.audience,
&meta.issuer,
);
assert!(r.is_ok(), "Call {} should succeed", i);
}
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(20));
}
#[test]
fn test_single_use_true_and_max_uses_gt_1_is_invalid() {
let store = MandateStore::memory().unwrap();
let mut meta = test_metadata();
meta.single_use = true;
meta.max_uses = Some(10);
let result = store.upsert_mandate(&meta);
assert!(matches!(
result,
Err(AuthzError::InvalidConstraints { max_uses: 10 })
));
}
#[test]
fn test_nonce_inserted_on_first_consume() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
consume(
&store,
&meta.mandate_id,
"tc_1",
Some("nonce_123"),
&meta.audience,
&meta.issuer,
)
.unwrap();
assert!(store
.nonce_exists(&meta.audience, &meta.issuer, "nonce_123")
.unwrap());
}
#[test]
fn test_nonce_replay_rejected() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let r1 = consume(
&store,
&meta.mandate_id,
"tc_1",
Some("nonce_123"),
&meta.audience,
&meta.issuer,
);
assert!(r1.is_ok());
let r2 = consume(
&store,
&meta.mandate_id,
"tc_2",
Some("nonce_123"),
&meta.audience,
&meta.issuer,
);
assert!(matches!(r2, Err(AuthzError::NonceReplay { nonce }) if nonce == "nonce_123"));
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(1));
}
#[test]
fn test_nonce_scope_is_audience_and_issuer() {
let store = MandateStore::memory().unwrap();
let meta1 = test_metadata();
let mut meta2 = test_metadata();
meta2.mandate_id = "sha256:test456".to_string();
meta2.audience = "different/app".to_string();
meta2.canonical_digest = "sha256:digest456".to_string();
store.upsert_mandate(&meta1).unwrap();
store.upsert_mandate(&meta2).unwrap();
let r1 = consume(
&store,
&meta1.mandate_id,
"tc_1",
Some("shared_nonce"),
&meta1.audience,
&meta1.issuer,
);
assert!(r1.is_ok());
let r2 = consume(
&store,
&meta2.mandate_id,
"tc_2",
Some("shared_nonce"),
&meta2.audience,
&meta2.issuer,
);
assert!(
r2.is_ok(),
"Same nonce with different audience should be allowed"
);
}
#[test]
fn test_multicall_produces_monotonic_counts_no_gaps() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let mut counts = Vec::new();
for i in 1..=50 {
let r = consume(
&store,
&meta.mandate_id,
&format!("tc_{}", i),
None,
&meta.audience,
&meta.issuer,
)
.unwrap();
counts.push(r.use_count);
}
let expected: Vec<u32> = (1..=50).collect();
assert_eq!(counts, expected);
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(50));
assert_eq!(store.count_uses(&meta.mandate_id).unwrap(), 50);
}
#[test]
fn test_multicall_idempotent_same_tool_call_id() {
let store = MandateStore::memory().unwrap();
let meta = test_metadata();
store.upsert_mandate(&meta).unwrap();
let mut receipts = Vec::new();
for _ in 0..20 {
let r = consume(
&store,
&meta.mandate_id,
"tc_same",
None,
&meta.audience,
&meta.issuer,
)
.unwrap();
receipts.push(r);
}
let first = &receipts[0];
for r in &receipts {
assert_eq!(r.use_id, first.use_id);
assert_eq!(r.use_count, first.use_count);
}
assert_eq!(store.get_use_count(&meta.mandate_id).unwrap(), Some(1));
assert_eq!(store.count_uses(&meta.mandate_id).unwrap(), 1);
}
#[test]
fn test_revocation_roundtrip() {
let store = MandateStore::memory().unwrap();
let revoked_at = Utc::now();
let record = RevocationRecord {
mandate_id: "sha256:revoked123".to_string(),
revoked_at,
reason: Some("User requested".to_string()),
revoked_by: Some("admin@example.com".to_string()),
source: Some("assay://myorg/myapp".to_string()),
event_id: Some("evt_revoke_001".to_string()),
};
store.upsert_revocation(&record).unwrap();
let got = store.get_revoked_at(&record.mandate_id).unwrap();
assert!(got.is_some());
let diff = (got.unwrap() - revoked_at).num_seconds().abs();
assert!(diff <= 1, "revoked_at timestamps differ by {}s", diff);
}
#[test]
fn test_revocation_is_revoked_helper() {
let store = MandateStore::memory().unwrap();
assert!(!store.is_revoked("sha256:not_revoked").unwrap());
store
.upsert_revocation(&RevocationRecord {
mandate_id: "sha256:is_revoked".to_string(),
revoked_at: Utc::now(),
reason: None,
revoked_by: None,
source: None,
event_id: None,
})
.unwrap();
assert!(store.is_revoked("sha256:is_revoked").unwrap());
}
#[test]
fn test_revocation_upsert_is_idempotent() {
let store = MandateStore::memory().unwrap();
let record = RevocationRecord {
mandate_id: "sha256:idem".to_string(),
revoked_at: Utc::now(),
reason: Some("First".to_string()),
revoked_by: None,
source: None,
event_id: None,
};
store.upsert_revocation(&record).unwrap();
store.upsert_revocation(&record).unwrap(); store.upsert_revocation(&record).unwrap();
assert!(store.is_revoked(&record.mandate_id).unwrap());
}
}