Skip to main content

api_testing_core/suite/
safety.rs

1use std::path::Path;
2
3use crate::Result;
4
5pub const MSG_NOT_SELECTED: &str = "not_selected";
6pub const MSG_SKIPPED_BY_ID: &str = "skipped_by_id";
7pub const MSG_TAG_MISMATCH: &str = "tag_mismatch";
8
9pub const MSG_WRITE_CASES_DISABLED: &str = "write_cases_disabled";
10pub const MSG_WRITE_CAPABLE_REQUIRES_ALLOW_WRITE_TRUE: &str =
11    "write_capable_case_requires_allowWrite_true";
12pub const MSG_MUTATION_REQUIRES_ALLOW_WRITE_TRUE: &str = "mutation_case_requires_allowWrite_true";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SafetyDecision {
16    Allow,
17    Skip(&'static str),
18    Fail(&'static str),
19}
20
21pub fn writes_enabled(allow_writes_flag: bool, effective_env: &str) -> bool {
22    allow_writes_flag || effective_env.trim().eq_ignore_ascii_case("local")
23}
24
25pub fn rest_method_is_write(method: &str) -> bool {
26    let m = method.trim().to_ascii_uppercase();
27    !matches!(m.as_str(), "GET" | "HEAD" | "OPTIONS")
28}
29
30pub fn graphql_operation_is_write_capable(
31    operation_file: &Path,
32    allow_write: bool,
33) -> Result<bool> {
34    if allow_write {
35        return Ok(true);
36    }
37    crate::graphql::mutation::operation_file_is_mutation(operation_file)
38}
39
40pub fn graphql_safety_decision(
41    operation_file: &Path,
42    allow_write: bool,
43    allow_writes_flag: bool,
44    effective_env: &str,
45) -> Result<SafetyDecision> {
46    let writes_enabled = writes_enabled(allow_writes_flag, effective_env);
47
48    // Defensive: explicit allowWrite=true is treated as write-capable even if mutation detection fails.
49    if allow_write && !writes_enabled {
50        return Ok(SafetyDecision::Skip(MSG_WRITE_CASES_DISABLED));
51    }
52
53    let is_mutation = crate::graphql::mutation::operation_file_is_mutation(operation_file)?;
54    if !is_mutation {
55        return Ok(SafetyDecision::Allow);
56    }
57
58    if !allow_write {
59        return Ok(SafetyDecision::Fail(MSG_MUTATION_REQUIRES_ALLOW_WRITE_TRUE));
60    }
61
62    if !writes_enabled {
63        return Ok(SafetyDecision::Skip(MSG_WRITE_CASES_DISABLED));
64    }
65
66    Ok(SafetyDecision::Allow)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    use tempfile::TempDir;
74
75    #[test]
76    fn suite_safety_rest_method_write_detection_matches_script_intent() {
77        assert!(!rest_method_is_write("GET"));
78        assert!(!rest_method_is_write("head"));
79        assert!(!rest_method_is_write(" OPTIONS "));
80        assert!(rest_method_is_write("POST"));
81        assert!(rest_method_is_write("PATCH"));
82        assert!(rest_method_is_write("DELETE"));
83    }
84
85    #[test]
86    fn suite_safety_graphql_allow_write_true_is_write_capable() {
87        let tmp = TempDir::new().unwrap();
88        let op = tmp.path().join("q.graphql");
89        std::fs::write(&op, "query Q { ok }\n").unwrap();
90
91        assert!(graphql_operation_is_write_capable(&op, true).unwrap());
92    }
93}