Skip to main content

sentry_options/
testing.rs

1//! Testing utilities for overriding option values in tests.
2//!
3//! # Example
4//!
5//! ```rust,ignore
6//! use sentry_options::Options;
7//! use sentry_options::testing::override_options;
8//! use serde_json::json;
9//!
10//! #[test]
11//! fn test_feature() {
12//!     let opts = Options::from_directory(test_dir).unwrap();
13//!
14//!     let _guard = override_options(&[("seer", "feature.enabled", json!(true))]).unwrap();
15//!     assert_eq!(opts.get("seer", "feature.enabled").unwrap(), json!(true));
16//! }
17//! ```
18
19use std::cell::RefCell;
20use std::collections::HashMap;
21
22use serde_json::Value;
23
24use crate::{GLOBAL_OPTIONS, Result};
25
26thread_local! {
27    static OVERRIDES: RefCell<HashMap<String, HashMap<String, Value>>> = RefCell::new(HashMap::new());
28}
29
30/// Set an override value for a specific namespace and key.
31pub fn set_override(namespace: &str, key: &str, value: Value) {
32    OVERRIDES.with(|o| {
33        o.borrow_mut()
34            .entry(namespace.to_string())
35            .or_default()
36            .insert(key.to_string(), value);
37    });
38}
39
40/// Get an override value if one exists.
41pub fn get_override(namespace: &str, key: &str) -> Option<Value> {
42    OVERRIDES.with(|o| {
43        o.borrow()
44            .get(namespace)
45            .and_then(|ns| ns.get(key).cloned())
46    })
47}
48
49/// Clear an override for a specific namespace and key.
50pub fn clear_override(namespace: &str, key: &str) {
51    OVERRIDES.with(|o| {
52        if let Some(ns_map) = o.borrow_mut().get_mut(namespace) {
53            ns_map.remove(key);
54        }
55    });
56}
57
58/// Guard that restores overrides when dropped.
59pub struct OverrideGuard {
60    previous: Vec<(String, String, Option<Value>)>,
61}
62
63impl Drop for OverrideGuard {
64    fn drop(&mut self) {
65        OVERRIDES.with(|o| {
66            let mut map = o.borrow_mut();
67            for (ns, key, prev_value) in self.previous.drain(..) {
68                match prev_value {
69                    Some(v) => {
70                        map.entry(ns).or_default().insert(key, v);
71                    }
72                    None => {
73                        if let Some(ns_map) = map.get_mut(&ns) {
74                            ns_map.remove(&key);
75                        }
76                    }
77                }
78            }
79        });
80    }
81}
82
83/// Set overrides for the lifetime of the returned guard.
84///
85/// Requires that options have been initialized via `init()`.
86/// Validates that each key exists in the schema and the value matches the expected type.
87/// When the guard is dropped (goes out of scope), the overrides are restored
88/// to their previous values.
89///
90/// # Note
91///
92/// Overrides are thread-local. They won't apply to spawned threads.
93///
94/// # Errors
95///
96/// Returns an error if:
97/// - Any namespace doesn't exist
98/// - Any key doesn't exist in the schema
99/// - Any value doesn't match the expected type
100///
101/// # Example
102///
103/// ```rust,ignore
104/// use sentry_options::testing::override_options;
105/// use sentry_options::init;
106/// use serde_json::json;
107///
108/// crate::init().unwrap();
109/// let _guard = override_options(&[
110///     ("namespace", "key1", json!(true)),
111///     ("namespace", "key2", json!(42)),
112/// ]).unwrap();
113/// // overrides are active here
114/// // when _guard goes out of scope, overrides are restored
115/// ```
116pub fn override_options(overrides: &[(&str, &str, Value)]) -> Result<OverrideGuard> {
117    // Validate all overrides before applying any
118    let opts = GLOBAL_OPTIONS
119        .get()
120        .ok_or(crate::OptionsError::NotInitialized)?;
121    for (ns, key, value) in overrides {
122        opts.validate_override(ns, key, value)?;
123    }
124
125    let mut previous = Vec::with_capacity(overrides.len());
126
127    OVERRIDES.with(|o| {
128        let mut map = o.borrow_mut();
129        for (ns, key, value) in overrides {
130            let prev = map.get(*ns).and_then(|m| m.get(*key).cloned());
131            previous.push((ns.to_string(), key.to_string(), prev));
132            map.entry(ns.to_string())
133                .or_default()
134                .insert(key.to_string(), value.clone());
135        }
136    });
137
138    Ok(OverrideGuard { previous })
139}
140
141/// A feature value that is enabled for every context.
142///
143/// Use with [`override_options`] to force a flag on in tests:
144///
145/// ```rust,ignore
146/// let _guard = override_options(&[(
147///     "seer",
148///     "feature.my-flag",
149///     always_on(),
150/// )]).unwrap();
151/// ```
152pub fn always_on() -> Value {
153    serde_json::json!({
154        "owner": {"team": "testing"},
155        "created_at": "1970-01-01",
156        "segments": [{"name": "always-on", "rollout": 100, "conditions": []}]
157    })
158}
159
160/// A feature value that is disabled for every context.
161///
162/// Use with [`override_options`] to force a flag off in tests.
163pub fn always_off() -> Value {
164    serde_json::json!({
165        "owner": {"team": "testing"},
166        "created_at": "1970-01-01",
167        "enabled": false,
168        "segments": []
169    })
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use serde_json::json;
176
177    #[test]
178    fn test_set_get_clear_override() {
179        set_override("ns", "key", json!(true));
180        assert_eq!(get_override("ns", "key"), Some(json!(true)));
181        clear_override("ns", "key");
182        assert_eq!(get_override("ns", "key"), None);
183    }
184
185    #[test]
186    fn test_override_guard_restores() {
187        crate::init().unwrap();
188        set_override("sentry-options-testing", "int-option", json!(1));
189
190        {
191            let _guard =
192                override_options(&[("sentry-options-testing", "int-option", json!(2))]).unwrap();
193            assert_eq!(
194                get_override("sentry-options-testing", "int-option"),
195                Some(json!(2))
196            );
197        }
198
199        assert_eq!(
200            get_override("sentry-options-testing", "int-option"),
201            Some(json!(1))
202        );
203        clear_override("sentry-options-testing", "int-option");
204    }
205
206    #[test]
207    fn test_override_guard_clears_new_key() {
208        crate::init().unwrap();
209        assert_eq!(get_override("sentry-options-testing", "bool-option"), None);
210
211        {
212            let _guard =
213                override_options(&[("sentry-options-testing", "bool-option", json!(true))])
214                    .unwrap();
215            assert_eq!(
216                get_override("sentry-options-testing", "bool-option"),
217                Some(json!(true))
218            );
219        }
220
221        assert_eq!(get_override("sentry-options-testing", "bool-option"), None);
222    }
223
224    #[test]
225    fn test_nested_overrides() {
226        crate::init().unwrap();
227        {
228            let _outer =
229                override_options(&[("sentry-options-testing", "int-option", json!(100))]).unwrap();
230            assert_eq!(
231                get_override("sentry-options-testing", "int-option"),
232                Some(json!(100))
233            );
234
235            {
236                let _inner =
237                    override_options(&[("sentry-options-testing", "int-option", json!(200))])
238                        .unwrap();
239                assert_eq!(
240                    get_override("sentry-options-testing", "int-option"),
241                    Some(json!(200))
242                );
243            }
244
245            assert_eq!(
246                get_override("sentry-options-testing", "int-option"),
247                Some(json!(100))
248            );
249        }
250
251        assert_eq!(get_override("sentry-options-testing", "int-option"), None);
252    }
253
254    #[test]
255    fn test_override_feature_flag() {
256        crate::init().unwrap();
257
258        let mut ctx = crate::FeatureContext::new();
259        ctx.insert("organization_id", json!(1));
260        ctx.identity_fields(vec!["organization_id"]);
261
262        // Baseline: no value in the store -> feature is off.
263        assert!(
264            !crate::features("sentry-options-testing").has("organizations:enabled-feature", &ctx)
265        );
266
267        {
268            let _guard = override_options(&[(
269                "sentry-options-testing",
270                "feature.organizations:enabled-feature",
271                always_on(),
272            )])
273            .unwrap();
274
275            assert!(
276                crate::features("sentry-options-testing")
277                    .has("organizations:enabled-feature", &ctx)
278            );
279        }
280
281        // Override removed when the guard drops.
282        assert!(
283            !crate::features("sentry-options-testing").has("organizations:enabled-feature", &ctx)
284        );
285    }
286
287    #[test]
288    fn test_always_off_forces_feature_off() {
289        crate::init().unwrap();
290
291        // enabled-feature matches org 123 in the test values.
292        let mut ctx = crate::FeatureContext::new();
293        ctx.insert("organization_id", json!(123));
294        ctx.identity_fields(vec!["organization_id"]);
295        assert!(
296            crate::features("sentry-options-testing").has("organizations:enabled-feature", &ctx)
297        );
298
299        {
300            let _guard = override_options(&[(
301                "sentry-options-testing",
302                "feature.organizations:enabled-feature",
303                always_off(),
304            )])
305            .unwrap();
306
307            assert!(
308                !crate::features("sentry-options-testing")
309                    .has("organizations:enabled-feature", &ctx)
310            );
311        }
312    }
313}