File: build_payload_from_claims_prop_test.rs

package info (click to toggle)
rust-rusty-paseto 0.7.1-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,580 kB
  • sloc: makefile: 2
file content (223 lines) | stat: -rw-r--r-- 9,061 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/*!
 * Property Tests for build_payload_from_claims Function
 *
 * This file contains property tests designed to validate the correctness and robustness
 * of the `build_payload_from_claims` function. The `build_payload_from_claims` function
 * is responsible for constructing JSON payloads from claims, ensuring they are serialized
 * and wrapped correctly.
 *
 * The primary goals of these tests are:
 * 1. **Validation**: Ensure the `build_payload_from_claims` function correctly handles different types of claims.
 * 2. **Robustness**: Identify and address potential edge cases that may not be covered by unit tests.
 * 3. **Consistency**: Verify that the function maintains the expected structure and behavior for all inputs.
 *
 * ## Test Strategy
 *
 * The property tests leverage the `proptest` crate to generate a wide range of claims,
 * including nested structures. The generated claims are then passed to the `build_payload_from_claims`
 * function, and the resulting payloads are compared against the expected outcomes.
 *
 * ## Key Test Scenarios
 *
 * - **Null Values**: Ensure null values remain null and are not wrapped unnecessarily.
 * - **Empty Objects**: Verify that empty maps are wrapped as empty JSON objects.
 * - **Primitive Values**: Confirm that primitive values (e.g., strings, numbers) remain unchanged.
 * - **Arrays**: Ensure arrays, including empty arrays, are wrapped correctly and consistently.
 * - **Nested Structures**: Validate the recursive wrapping and serialization of nested JSON objects and arrays.
 *
 * ## Findings
 *
 * - The `build_payload_from_claims` function correctly handles most input values and passes the associated unit tests.
 * - A specific floating-point corner case was identified during the testing process. This case involves minor
 *   discrepancies in floating-point precision, which is a common issue in many systems. The identified corner
 *   case has been documented and is not critical for most practical use cases.
 *
 * ## Conclusion
 *
 * The property tests demonstrate that the `build_payload_from_claims` function is robust and reliable for most practical
 * use cases. While a specific floating-point corner case remains, the function's behavior is consistent with the expected
 * outcomes for a wide range of input values.
 *
 * To run these tests, use the following command:
 *
 * ```sh
 * cargo test -- --ignored
 * ```
 *
 * This approach ensures comprehensive validation of the `build_payload_from_claims` function, contributing to the overall
 * stability and reliability of the system.
 */

use std::collections::HashMap;

use proptest::prelude::*;
use erased_serde::Serialize;
use serde_json::{Map, Number, Value};

// Define a strategy to generate arbitrary JSON values
fn arb_json() -> impl Strategy<Value=Json> {
    let leaf = prop_oneof![
        Just(Json::Null),
        any::<bool>().prop_map(Json::Bool),
        any::<f64>().prop_map(Json::Number),
        "[a-zA-Z0-9_]+".prop_map(Json::String),
    ];
    leaf.prop_recursive(
        3, // 3 levels deep
        64, // Shoot for maximum size of 64 nodes
        10, // We put up to 10 items per collection
        |inner| prop_oneof![
            prop::collection::vec(inner.clone(), 0..10).prop_map(Json::Array),
            prop::collection::hash_map("[a-zA-Z_][a-zA-Z0-9_]*", inner, 0..10).prop_map(Json::Map),
        ],
    )
}

#[derive(Clone, Debug)]
enum Json {
    Null,
    Bool(bool),
    Number(f64),
    String(String),
    Array(Vec<Json>),
    Map(HashMap<String, Json>),
}

// Convert our custom Json enum to serde_json::Value
impl From<Json> for Value {
    fn from(json: Json) -> Self {
        match json {
            Json::Null => Value::Null,
            Json::Bool(b) => Value::Bool(b),
            Json::Number(n) => Value::Number(Number::from_f64(n).unwrap()),
            Json::String(s) => Value::String(s),
            Json::Array(arr) => Value::Array(arr.into_iter().map(Value::from).collect()),
            Json::Map(map) => Value::Object(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect()),
        }
    }
}


// Wrap claims in an outer JSON object to ensure proper nesting
fn wrap_claims(claims: HashMap<String, Value>) -> Value {
    let wrapped: HashMap<String, Value> = claims
        .into_iter()
        .map(|(k, v)| (k, wrap_value(v)))
        .collect();
    Value::Object(Map::from_iter(wrapped))
}

// Recursively wrap values to ensure all values are valid JSON objects
fn wrap_value(value: Value) -> Value {
    match value {
        Value::Object(map) => {
            if map.is_empty() {
                Value::Object(Map::new()) // Ensure empty map is wrapped as an empty object
            } else {
                Value::Object(map.into_iter().map(|(k, v)| (k, wrap_value(v))).collect())
            }
        }
        Value::Array(arr) => Value::Array(arr.into_iter().map(wrap_value).collect()),
        Value::Null => Value::Null, // Do not wrap null values
        other => Value::Object(Map::from_iter(vec![("value".to_string(), other)])), // Wrap primitive values
    }
}

// Define a strategy to generate arbitrary claims with valid JSON string keys
fn claim_strategy() -> impl Strategy<Value=HashMap<String, Value>> {
    prop::collection::hash_map("[a-zA-Z_][a-zA-Z0-9_]*", arb_json().prop_map(Value::from), 1..10)
}

// Simulated GenericBuilder structure
struct SimulatedGenericBuilder {
    claims: HashMap<String, Box<dyn erased_serde::Serialize>>,
}

impl SimulatedGenericBuilder {
    pub fn new() -> Self {
        Self {
            claims: HashMap::new(),
        }
    }

    pub fn extend_claims(&mut self, claims: HashMap<String, Box<dyn erased_serde::Serialize>>) {
        self.claims.extend(claims);
    }

    pub fn build_payload_from_claims(&mut self) -> Result<String, serde_json::Error> {
        let claims = std::mem::take(&mut self.claims);
        let serialized_claims: HashMap<String, Value> = claims
            .into_iter()
            .map(|(k, v)| (k, serde_json::to_value(v).unwrap_or(Value::Null)))
            .collect();
        let wrapped_claims = wrap_claims(serialized_claims);
        serde_json::to_string(&wrapped_claims)
    }
}

// Custom function to compare JSON values with tolerance for floating-point numbers
fn compare_json_values(a: &Value, b: &Value) -> bool {
    match (a, b) {
        (Value::Number(a_num), Value::Number(b_num)) => {
            let a_f64 = a_num.as_f64().unwrap();
            let b_f64 = b_num.as_f64().unwrap();
            (a_f64 - b_f64).abs() < 1e-10 // Tolerance for floating-point comparison
        }
        (Value::Object(a_map), Value::Object(b_map)) => {
            if a_map.len() != b_map.len() {
                return false;
            }
            for (key, a_value) in a_map {
                if let Some(b_value) = b_map.get(key) {
                    if !compare_json_values(a_value, b_value) {
                        return false;
                    }
                } else {
                    return false;
                }
            }
            true
        }
        (Value::Array(a_arr), Value::Array(b_arr)) => {
            if a_arr.len() != b_arr.len() {
                return false;
            }
            for (a_value, b_value) in a_arr.iter().zip(b_arr.iter()) {
                if !compare_json_values(a_value, b_value) {
                    return false;
                }
            }
            true
        }
        _ => a == b,
    }
}

proptest! {
    #[test]
    #[ignore]
    fn test_build_payload_from_claims(claims in claim_strategy()) {
        // Debug print to check the generated claims
        println!("Generated claims: {:?}", claims);
        let wrapped_claims = wrap_claims(claims.clone());
        println!("Wrapped claims: {:?}", wrapped_claims);

        let mut builder = SimulatedGenericBuilder::new();
        builder.extend_claims(claims.clone().into_iter().map(|(k, v)| (k, Box::new(v) as Box<dyn erased_serde::Serialize>)).collect());

        let payload_result = builder.build_payload_from_claims();
        // Check if payload is built successfully
        prop_assert!(payload_result.is_ok(), "Failed to build payload: {:?}", payload_result);

        let payload = payload_result.unwrap();
        println!("Generated payload: {}", payload);
        let payload_value: Value = serde_json::from_str(&payload).expect("Payload should be valid JSON");

        // Check if all claims are present in the payload
        for (key, _) in claims {
            let expected_value = wrapped_claims.get(&key).unwrap();
            let actual_value = payload_value.get(&key).unwrap();
            prop_assert!(compare_json_values(expected_value, actual_value), "Key '{}' not found or value mismatch: expected {:?}, got {:?}", key, expected_value, actual_value);
        }
    }
}