use super::store::{IdempotencyError, IdempotencyStore};
use super::types::{IdempotencyStatus, RequestFingerprint, ScopedKey};
use serde_json::Value;
pub enum IdempotencyCheckResult {
NoKey,
Replay {
response: Value,
key: String,
status: IdempotencyStatus,
},
Execute {
key: ScopedKey,
fingerprint: RequestFingerprint,
},
}
pub struct IdempotencyHandler {
store: IdempotencyStore,
}
impl IdempotencyHandler {
pub fn new() -> Result<Self, IdempotencyError> {
Ok(Self {
store: IdempotencyStore::new()?,
})
}
pub fn check(
&self,
idempotency_key: Option<String>,
team_id: String,
user_id: String,
method: String,
params: &serde_json::Map<String, Value>,
) -> Result<IdempotencyCheckResult, IdempotencyError> {
let Some(key_str) = idempotency_key else {
return Ok(IdempotencyCheckResult::NoKey);
};
let scoped_key = ScopedKey::new(team_id, user_id, method, key_str.clone());
let fingerprint = RequestFingerprint::from_params(params);
match self.store.check(&scoped_key, &fingerprint)? {
Some(response) => Ok(IdempotencyCheckResult::Replay {
response,
key: key_str,
status: IdempotencyStatus::Replayed,
}),
None => Ok(IdempotencyCheckResult::Execute {
key: scoped_key,
fingerprint,
}),
}
}
pub fn store(
&mut self,
key: ScopedKey,
fingerprint: RequestFingerprint,
response: Value,
) -> Result<(), IdempotencyError> {
self.store.put(key, fingerprint, response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::TempDir;
fn create_test_handler() -> (IdempotencyHandler, TempDir) {
let temp_dir = TempDir::new().unwrap();
let store_path = temp_dir.path().join("idempotency_store.json");
let store = IdempotencyStore::with_path(store_path).unwrap();
(IdempotencyHandler { store }, temp_dir)
}
#[test]
fn test_no_key() {
let (handler, _temp) = create_test_handler();
let params = serde_json::Map::new();
let result = handler
.check(
None,
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
¶ms,
)
.unwrap();
assert!(matches!(result, IdempotencyCheckResult::NoKey));
}
#[test]
fn test_execute_first_time() {
let (handler, _temp) = create_test_handler();
let mut params = serde_json::Map::new();
params.insert("channel".into(), json!("C123"));
params.insert("text".into(), json!("hello"));
let result = handler
.check(
Some("test-key-1".into()),
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
¶ms,
)
.unwrap();
assert!(matches!(result, IdempotencyCheckResult::Execute { .. }));
}
#[test]
fn test_replay_second_time() {
let (mut handler, _temp) = create_test_handler();
let mut params = serde_json::Map::new();
params.insert("channel".into(), json!("C123"));
params.insert("text".into(), json!("hello"));
let result = handler
.check(
Some("test-key-2".into()),
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
¶ms,
)
.unwrap();
let (key, fingerprint) = match result {
IdempotencyCheckResult::Execute { key, fingerprint } => (key, fingerprint),
_ => panic!("Expected Execute"),
};
let response = json!({"ok": true, "ts": "1234567890.123456"});
handler.store(key, fingerprint, response.clone()).unwrap();
let result2 = handler
.check(
Some("test-key-2".into()),
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
¶ms,
)
.unwrap();
match result2 {
IdempotencyCheckResult::Replay { response: r, .. } => {
assert_eq!(r, response);
}
_ => panic!("Expected Replay"),
}
}
#[test]
fn test_fingerprint_mismatch_error() {
let (mut handler, _temp) = create_test_handler();
let mut params1 = serde_json::Map::new();
params1.insert("channel".into(), json!("C123"));
params1.insert("text".into(), json!("hello"));
let result = handler
.check(
Some("test-key-3".into()),
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
¶ms1,
)
.unwrap();
let (key, fingerprint) = match result {
IdempotencyCheckResult::Execute { key, fingerprint } => (key, fingerprint),
_ => panic!("Expected Execute"),
};
let response = json!({"ok": true});
handler.store(key, fingerprint, response).unwrap();
let mut params2 = serde_json::Map::new();
params2.insert("channel".into(), json!("C123"));
params2.insert("text".into(), json!("goodbye"));
let result2 = handler.check(
Some("test-key-3".into()),
"T123".into(),
"U456".into(),
"chat.postMessage".into(),
¶ms2,
);
assert!(matches!(
result2,
Err(IdempotencyError::FingerprintMismatch)
));
}
}