Skip to main content

slack_rs/idempotency/
handler.rs

1//! Idempotency handler for write operations
2
3use super::store::{IdempotencyError, IdempotencyStore};
4use super::types::{IdempotencyStatus, RequestFingerprint, ScopedKey};
5use serde_json::Value;
6
7/// Result of idempotency check
8pub enum IdempotencyCheckResult {
9    /// No idempotency key provided, proceed normally
10    NoKey,
11    /// Key provided and operation should be replayed
12    Replay {
13        response: Value,
14        key: String,
15        status: IdempotencyStatus,
16    },
17    /// Key provided but no cached result, proceed and store
18    Execute {
19        key: ScopedKey,
20        fingerprint: RequestFingerprint,
21    },
22}
23
24/// Idempotency handler for write operations
25pub struct IdempotencyHandler {
26    store: IdempotencyStore,
27}
28
29impl IdempotencyHandler {
30    /// Create a new handler
31    pub fn new() -> Result<Self, IdempotencyError> {
32        Ok(Self {
33            store: IdempotencyStore::new()?,
34        })
35    }
36
37    /// Check if operation should be executed or replayed
38    ///
39    /// # Arguments
40    /// * `idempotency_key` - Optional idempotency key
41    /// * `team_id` - Team ID
42    /// * `user_id` - User ID
43    /// * `method` - API method name
44    /// * `params` - Request parameters for fingerprinting
45    ///
46    /// # Returns
47    /// * `Ok(IdempotencyCheckResult)` with next action
48    /// * `Err(IdempotencyError)` if fingerprint mismatch or other error
49    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    /// Store operation result
78    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                &params,
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                &params,
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        // First execution - should execute
147        let result = handler
148            .check(
149                Some("test-key-2".into()),
150                "T123".into(),
151                "U456".into(),
152                "chat.postMessage".into(),
153                &params,
154            )
155            .unwrap();
156
157        let (key, fingerprint) = match result {
158            IdempotencyCheckResult::Execute { key, fingerprint } => (key, fingerprint),
159            _ => panic!("Expected Execute"),
160        };
161
162        // Store result
163        let response = json!({"ok": true, "ts": "1234567890.123456"});
164        handler.store(key, fingerprint, response.clone()).unwrap();
165
166        // Second execution - should replay
167        let result2 = handler
168            .check(
169                Some("test-key-2".into()),
170                "T123".into(),
171                "U456".into(),
172                "chat.postMessage".into(),
173                &params,
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        // First request
190        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                &params1,
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        // Second request with different params but same key - should error
213        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            &params2,
223        );
224
225        assert!(matches!(
226            result2,
227            Err(IdempotencyError::FingerprintMismatch)
228        ));
229    }
230}