Skip to main content

oag_fastapi_server/emitters/
tests.rs

1use minijinja::{Environment, context};
2use oag_core::GeneratedFile;
3use oag_core::ir::{HttpMethod, IrOperation, IrParameterLocation, IrReturnType, IrSpec, IrType};
4
5/// Emit `conftest.py` + `test_routes.py` for pytest.
6pub fn emit_tests(ir: &IrSpec) -> Vec<GeneratedFile> {
7    vec![
8        GeneratedFile {
9            path: "conftest.py".to_string(),
10            content: include_str!("../../templates/conftest.py.j2").to_string(),
11        },
12        GeneratedFile {
13            path: "test_routes.py".to_string(),
14            content: emit_test_routes(ir),
15        },
16    ]
17}
18
19fn emit_test_routes(ir: &IrSpec) -> String {
20    let mut env = Environment::new();
21    env.add_template(
22        "test_routes.py.j2",
23        include_str!("../../templates/test_routes.py.j2"),
24    )
25    .expect("template should be valid");
26    let tmpl = env.get_template("test_routes.py.j2").unwrap();
27
28    // Collect model names referenced in request bodies for imports
29    let model_imports: Vec<String> = ir
30        .operations
31        .iter()
32        .filter_map(|op| {
33            op.request_body.as_ref().and_then(|b| match &b.body_type {
34                IrType::Ref(name) => Some(name.clone()),
35                _ => None,
36            })
37        })
38        .collect::<std::collections::BTreeSet<_>>()
39        .into_iter()
40        .collect();
41
42    let operations: Vec<minijinja::Value> = ir
43        .operations
44        .iter()
45        .flat_map(build_test_operation_contexts)
46        .collect();
47
48    tmpl.render(context! {
49        operations => operations,
50        model_imports => model_imports,
51    })
52    .expect("render should succeed")
53}
54
55fn build_test_operation_contexts(op: &IrOperation) -> Vec<minijinja::Value> {
56    let mut results = Vec::new();
57
58    let http_method = match op.method {
59        HttpMethod::Get => "get",
60        HttpMethod::Post => "post",
61        HttpMethod::Put => "put",
62        HttpMethod::Delete => "delete",
63        HttpMethod::Patch => "patch",
64        _ => "get",
65    };
66
67    // Replace path params with placeholder values for test URLs
68    let test_path = build_test_path(&op.path, op);
69    let has_body = op.request_body.is_some();
70    let mock_body = op
71        .request_body
72        .as_ref()
73        .map(|b| mock_value_python(&b.body_type))
74        .unwrap_or_else(|| "{}".to_string());
75
76    match &op.return_type {
77        IrReturnType::Standard(_) => {
78            results.push(context! {
79                kind => "standard",
80                name => op.name.snake_case.clone(),
81                http_method => http_method,
82                path => op.path.clone(),
83                test_path => test_path,
84                has_body => has_body,
85                mock_body => mock_body,
86            });
87        }
88        IrReturnType::Void => {
89            results.push(context! {
90                kind => "void",
91                name => op.name.snake_case.clone(),
92                http_method => http_method,
93                path => op.path.clone(),
94                test_path => test_path,
95                has_body => has_body,
96                mock_body => mock_body,
97            });
98        }
99        IrReturnType::Sse(sse) => {
100            results.push(context! {
101                kind => "sse",
102                name => op.name.snake_case.clone(),
103                http_method => http_method,
104                path => op.path.clone(),
105                test_path => test_path,
106                has_body => has_body,
107                mock_body => mock_body,
108            });
109
110            // Also test the JSON endpoint if dual
111            if sse.json_response.is_some() {
112                results.push(context! {
113                    kind => "standard",
114                    name => op.name.snake_case.clone(),
115                    http_method => http_method,
116                    path => op.path.clone(),
117                    test_path => test_path,
118                    has_body => has_body,
119                    mock_body => mock_body,
120                });
121            }
122        }
123    }
124
125    results
126}
127
128/// Replace `{param}` placeholders in the path with test values.
129fn build_test_path(path: &str, op: &IrOperation) -> String {
130    let mut result = path.to_string();
131    for param in &op.parameters {
132        if param.location == IrParameterLocation::Path {
133            let placeholder = format!("{{{}}}", param.original_name);
134            let test_value = mock_path_value(&param.param_type);
135            result = result.replace(&placeholder, &test_value);
136        }
137    }
138    result
139}
140
141/// Generate a mock path parameter value.
142fn mock_path_value(ir_type: &IrType) -> String {
143    match ir_type {
144        IrType::Integer => "1".to_string(),
145        IrType::Number => "1".to_string(),
146        IrType::String | IrType::DateTime => "test".to_string(),
147        _ => "test".to_string(),
148    }
149}
150
151/// Generate a mock Python value for a given IrType (for request bodies).
152fn mock_value_python(ir_type: &IrType) -> String {
153    match ir_type {
154        IrType::String | IrType::DateTime => "\"test\"".to_string(),
155        IrType::StringLiteral(s) => format!("\"{s}\""),
156        IrType::Number | IrType::Integer => "1".to_string(),
157        IrType::Boolean => "True".to_string(),
158        IrType::Null | IrType::Void => "None".to_string(),
159        IrType::Array(_) => "[]".to_string(),
160        IrType::Ref(name) => format!("{}.model_construct()", name),
161        IrType::Object(_) | IrType::Map(_) | IrType::Any => "{}".to_string(),
162        IrType::Binary => "b\"test\"".to_string(),
163        IrType::Union(variants) | IrType::Intersection(variants) => {
164            if let Some(first) = variants.first() {
165                mock_value_python(first)
166            } else {
167                "{}".to_string()
168            }
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_mock_path_value() {
179        assert_eq!(mock_path_value(&IrType::Integer), "1");
180        assert_eq!(mock_path_value(&IrType::String), "test");
181    }
182
183    #[test]
184    fn test_mock_value_python() {
185        assert_eq!(mock_value_python(&IrType::String), "\"test\"");
186        assert_eq!(mock_value_python(&IrType::Integer), "1");
187        assert_eq!(mock_value_python(&IrType::Boolean), "True");
188        assert_eq!(
189            mock_value_python(&IrType::Array(Box::new(IrType::String))),
190            "[]"
191        );
192        assert_eq!(
193            mock_value_python(&IrType::Ref("Pet".to_string())),
194            "Pet.model_construct()"
195        );
196    }
197}