1use super::store::{IdempotencyError, IdempotencyStore};
4use super::types::{IdempotencyStatus, RequestFingerprint, ScopedKey};
5use serde_json::Value;
6
7pub enum IdempotencyCheckResult {
9 NoKey,
11 Replay {
13 response: Value,
14 key: String,
15 status: IdempotencyStatus,
16 },
17 Execute {
19 key: ScopedKey,
20 fingerprint: RequestFingerprint,
21 },
22}
23
24pub struct IdempotencyHandler {
26 store: IdempotencyStore,
27}
28
29impl IdempotencyHandler {
30 pub fn new() -> Result<Self, IdempotencyError> {
32 Ok(Self {
33 store: IdempotencyStore::new()?,
34 })
35 }
36
37 pub fn check(
50 &self,
51 idempotency_key: Option<String>,
52 team_id: String,
53 user_id: String,
54 method: String,
55 params: &serde_json::Map<String, Value>,
56 ) -> Result<IdempotencyCheckResult, IdempotencyError> {
57 let Some(key_str) = idempotency_key else {
58 return Ok(IdempotencyCheckResult::NoKey);
59 };
60
61 let scoped_key = ScopedKey::new(team_id, user_id, method, key_str.clone());
62 let fingerprint = RequestFingerprint::from_params(params);
63
64 match self.store.check(&scoped_key, &fingerprint)? {
65 Some(response) => Ok(IdempotencyCheckResult::Replay {
66 response,
67 key: key_str,
68 status: IdempotencyStatus::Replayed,
69 }),
70 None => Ok(IdempotencyCheckResult::Execute {
71 key: scoped_key,
72 fingerprint,
73 }),
74 }
75 }
76
77 pub fn store(
79 &mut self,
80 key: ScopedKey,
81 fingerprint: RequestFingerprint,
82 response: Value,
83 ) -> Result<(), IdempotencyError> {
84 self.store.put(key, fingerprint, response)
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use serde_json::json;
92 use tempfile::TempDir;
93
94 fn create_test_handler() -> (IdempotencyHandler, TempDir) {
95 let temp_dir = TempDir::new().unwrap();
96 let store_path = temp_dir.path().join("idempotency_store.json");
97 let store = IdempotencyStore::with_path(store_path).unwrap();
98 (IdempotencyHandler { store }, temp_dir)
99 }
100
101 #[test]
102 fn test_no_key() {
103 let (handler, _temp) = create_test_handler();
104 let params = serde_json::Map::new();
105
106 let result = handler
107 .check(
108 None,
109 "T123".into(),
110 "U456".into(),
111 "chat.postMessage".into(),
112 ¶ms,
113 )
114 .unwrap();
115
116 assert!(matches!(result, IdempotencyCheckResult::NoKey));
117 }
118
119 #[test]
120 fn test_execute_first_time() {
121 let (handler, _temp) = create_test_handler();
122 let mut params = serde_json::Map::new();
123 params.insert("channel".into(), json!("C123"));
124 params.insert("text".into(), json!("hello"));
125
126 let result = handler
127 .check(
128 Some("test-key-1".into()),
129 "T123".into(),
130 "U456".into(),
131 "chat.postMessage".into(),
132 ¶ms,
133 )
134 .unwrap();
135
136 assert!(matches!(result, IdempotencyCheckResult::Execute { .. }));
137 }
138
139 #[test]
140 fn test_replay_second_time() {
141 let (mut handler, _temp) = create_test_handler();
142 let mut params = serde_json::Map::new();
143 params.insert("channel".into(), json!("C123"));
144 params.insert("text".into(), json!("hello"));
145
146 let result = handler
148 .check(
149 Some("test-key-2".into()),
150 "T123".into(),
151 "U456".into(),
152 "chat.postMessage".into(),
153 ¶ms,
154 )
155 .unwrap();
156
157 let (key, fingerprint) = match result {
158 IdempotencyCheckResult::Execute { key, fingerprint } => (key, fingerprint),
159 _ => panic!("Expected Execute"),
160 };
161
162 let response = json!({"ok": true, "ts": "1234567890.123456"});
164 handler.store(key, fingerprint, response.clone()).unwrap();
165
166 let result2 = handler
168 .check(
169 Some("test-key-2".into()),
170 "T123".into(),
171 "U456".into(),
172 "chat.postMessage".into(),
173 ¶ms,
174 )
175 .unwrap();
176
177 match result2 {
178 IdempotencyCheckResult::Replay { response: r, .. } => {
179 assert_eq!(r, response);
180 }
181 _ => panic!("Expected Replay"),
182 }
183 }
184
185 #[test]
186 fn test_fingerprint_mismatch_error() {
187 let (mut handler, _temp) = create_test_handler();
188
189 let mut params1 = serde_json::Map::new();
191 params1.insert("channel".into(), json!("C123"));
192 params1.insert("text".into(), json!("hello"));
193
194 let result = handler
195 .check(
196 Some("test-key-3".into()),
197 "T123".into(),
198 "U456".into(),
199 "chat.postMessage".into(),
200 ¶ms1,
201 )
202 .unwrap();
203
204 let (key, fingerprint) = match result {
205 IdempotencyCheckResult::Execute { key, fingerprint } => (key, fingerprint),
206 _ => panic!("Expected Execute"),
207 };
208
209 let response = json!({"ok": true});
210 handler.store(key, fingerprint, response).unwrap();
211
212 let mut params2 = serde_json::Map::new();
214 params2.insert("channel".into(), json!("C123"));
215 params2.insert("text".into(), json!("goodbye"));
216
217 let result2 = handler.check(
218 Some("test-key-3".into()),
219 "T123".into(),
220 "U456".into(),
221 "chat.postMessage".into(),
222 ¶ms2,
223 );
224
225 assert!(matches!(
226 result2,
227 Err(IdempotencyError::FingerprintMismatch)
228 ));
229 }
230}