Skip to main content

durable_execution_sdk_testing/checkpoint_server/
checkpoint_token.rs

1//! Checkpoint token encoding and decoding utilities.
2//!
3//! This module provides utilities for encoding and decoding checkpoint tokens,
4//! which are used to track the state of an execution across invocations.
5
6use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
7use serde::{Deserialize, Serialize};
8
9use crate::error::TestError;
10
11use super::types::{CheckpointToken, ExecutionId, InvocationId};
12
13/// Data contained in a checkpoint token.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15pub struct CheckpointTokenData {
16    /// The execution ID
17    pub execution_id: ExecutionId,
18    /// A unique token for this checkpoint
19    pub token: String,
20    /// The invocation ID
21    pub invocation_id: InvocationId,
22}
23
24/// Encode checkpoint token data to a base64 string.
25///
26/// # Arguments
27///
28/// * `data` - The checkpoint token data to encode
29///
30/// # Returns
31///
32/// A base64-encoded string representation of the token data.
33pub fn encode_checkpoint_token(data: &CheckpointTokenData) -> CheckpointToken {
34    let json = serde_json::to_string(data).expect("CheckpointTokenData should serialize");
35    URL_SAFE_NO_PAD.encode(json.as_bytes())
36}
37
38/// Decode a checkpoint token string to data.
39///
40/// # Arguments
41///
42/// * `token` - The base64-encoded checkpoint token string
43///
44/// # Returns
45///
46/// The decoded checkpoint token data, or an error if decoding fails.
47pub fn decode_checkpoint_token(token: &str) -> Result<CheckpointTokenData, TestError> {
48    let bytes = URL_SAFE_NO_PAD
49        .decode(token)
50        .map_err(|e| TestError::InvalidCheckpointToken(format!("base64 decode error: {}", e)))?;
51
52    let json = String::from_utf8(bytes)
53        .map_err(|e| TestError::InvalidCheckpointToken(format!("utf8 decode error: {}", e)))?;
54
55    serde_json::from_str(&json)
56        .map_err(|e| TestError::InvalidCheckpointToken(format!("json parse error: {}", e)))
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_encode_decode_roundtrip() {
65        let data = CheckpointTokenData {
66            execution_id: "exec-123".to_string(),
67            token: "token-abc".to_string(),
68            invocation_id: "inv-456".to_string(),
69        };
70
71        let encoded = encode_checkpoint_token(&data);
72        let decoded = decode_checkpoint_token(&encoded).unwrap();
73
74        assert_eq!(data, decoded);
75    }
76
77    #[test]
78    fn test_decode_invalid_base64() {
79        let result = decode_checkpoint_token("not-valid-base64!!!");
80        assert!(result.is_err());
81        match result {
82            Err(TestError::InvalidCheckpointToken(msg)) => {
83                assert!(msg.contains("base64"));
84            }
85            _ => panic!("Expected InvalidCheckpointToken error"),
86        }
87    }
88
89    #[test]
90    fn test_decode_invalid_json() {
91        // Valid base64 but not valid JSON
92        let invalid_json = URL_SAFE_NO_PAD.encode(b"not json");
93        let result = decode_checkpoint_token(&invalid_json);
94        assert!(result.is_err());
95        match result {
96            Err(TestError::InvalidCheckpointToken(msg)) => {
97                assert!(msg.contains("json"));
98            }
99            _ => panic!("Expected InvalidCheckpointToken error"),
100        }
101    }
102
103    #[test]
104    fn test_encode_produces_url_safe_string() {
105        let data = CheckpointTokenData {
106            execution_id: "exec/with+special=chars".to_string(),
107            token: "token".to_string(),
108            invocation_id: "inv".to_string(),
109        };
110
111        let encoded = encode_checkpoint_token(&data);
112
113        // URL-safe base64 should not contain +, /, or =
114        assert!(!encoded.contains('+'));
115        assert!(!encoded.contains('/'));
116        // URL_SAFE_NO_PAD doesn't use padding
117    }
118}