Skip to main content

iac_forge/testing/
mod.rs

1//! Shared test fixtures for iac-forge backends.
2//!
3//! Use these helpers in backend tests to avoid duplicating test data construction.
4
5pub mod fixtures;
6
7use std::collections::BTreeMap;
8
9use crate::ir::{
10    AuthInfo, CrudInfo, IacAttribute, IacDataSource, IacProvider, IacResource, IacType,
11    IdentityInfo,
12};
13
14/// Create a minimal test provider.
15#[must_use]
16pub fn test_provider(name: &str) -> IacProvider {
17    IacProvider {
18        name: name.to_string(),
19        description: format!("{name} test provider"),
20        version: "1.0.0".to_string(),
21        auth: AuthInfo {
22            token_field: "token".to_string(),
23            env_var: format!("{}_TOKEN", name.to_uppercase()),
24            gateway_url_field: "api_url".to_string(),
25            gateway_env_var: format!("{}_API_URL", name.to_uppercase()),
26        },
27        skip_fields: vec!["token".to_string()],
28        platform_config: BTreeMap::new(),
29    }
30}
31
32/// Create a test resource with typical fields (string required+immutable, string sensitive, list optional).
33///
34/// Creates three attributes:
35/// - `"name"`: String, required, immutable, `read_path = "item_name"`
36/// - `"value"`: String, required, sensitive
37/// - `"tags"`: `List(String)`, optional, `read_path = "item_tags"`
38#[must_use]
39pub fn test_resource(name: &str) -> IacResource {
40    IacResource {
41        name: name.to_string(),
42        description: format!("{name} test resource"),
43        category: "test".to_string(),
44        crud: CrudInfo {
45            create_endpoint: format!("/create-{name}"),
46            create_schema: format!("Create{name}"),
47            update_endpoint: None,
48            update_schema: None,
49            read_endpoint: format!("/get-{name}"),
50            read_schema: format!("Get{name}"),
51            read_response_schema: None,
52            delete_endpoint: format!("/delete-{name}"),
53            delete_schema: format!("Delete{name}"),
54        },
55        attributes: vec![
56            TestAttributeBuilder::new("name", IacType::String)
57                .required()
58                .immutable()
59                .read_path("item_name")
60                .description("The resource name")
61                .build(),
62            TestAttributeBuilder::new("value", IacType::String)
63                .required()
64                .sensitive()
65                .description("The resource value")
66                .build(),
67            TestAttributeBuilder::new("tags", IacType::List(Box::new(IacType::String)))
68                .read_path("item_tags")
69                .description("Resource tags")
70                .build(),
71        ],
72        identity: IdentityInfo {
73            id_field: "name".to_string(),
74            import_field: "name".to_string(),
75            force_replace_fields: vec!["name".to_string()],
76        },
77        read_mapping: BTreeMap::new(),
78    }
79}
80
81/// Create a test resource with a single attribute of a given type.
82#[must_use]
83pub fn test_resource_with_type(name: &str, attr_name: &str, iac_type: IacType) -> IacResource {
84    IacResource {
85        name: name.to_string(),
86        description: format!("{name} test resource"),
87        category: "test".to_string(),
88        crud: CrudInfo {
89            create_endpoint: format!("/create-{name}"),
90            create_schema: format!("Create{name}"),
91            update_endpoint: None,
92            update_schema: None,
93            read_endpoint: format!("/get-{name}"),
94            read_schema: format!("Get{name}"),
95            read_response_schema: None,
96            delete_endpoint: format!("/delete-{name}"),
97            delete_schema: format!("Delete{name}"),
98        },
99        attributes: vec![
100            TestAttributeBuilder::new(attr_name, iac_type)
101                .required()
102                .build(),
103        ],
104        identity: IdentityInfo {
105            id_field: attr_name.to_string(),
106            import_field: attr_name.to_string(),
107            force_replace_fields: vec![],
108        },
109        read_mapping: BTreeMap::new(),
110    }
111}
112
113/// Create a test data source with typical fields.
114///
115/// Creates two attributes:
116/// - `"name"`: String, required (not computed)
117/// - `"value"`: String, computed
118#[must_use]
119pub fn test_data_source(name: &str) -> IacDataSource {
120    IacDataSource {
121        name: name.to_string(),
122        description: format!("{name} test data source"),
123        read_endpoint: format!("/get-{name}"),
124        read_schema: format!("Get{name}"),
125        read_response_schema: None,
126        attributes: vec![
127            TestAttributeBuilder::new("name", IacType::String)
128                .required()
129                .description("The data source name")
130                .build(),
131            TestAttributeBuilder::new("value", IacType::String)
132                .computed()
133                .description("The data source value")
134                .build(),
135        ],
136        read_mapping: BTreeMap::new(),
137    }
138}
139
140/// Builder for constructing test `IacAttribute` values with sensible defaults.
141#[allow(clippy::struct_excessive_bools)]
142pub struct TestAttributeBuilder {
143    api_name: String,
144    canonical_name: String,
145    description: String,
146    iac_type: IacType,
147    required: bool,
148    optional: bool,
149    computed: bool,
150    sensitive: bool,
151    json_encoded: bool,
152    immutable: bool,
153    default_value: Option<serde_json::Value>,
154    enum_values: Option<Vec<String>>,
155    read_path: Option<String>,
156    update_only: bool,
157}
158
159impl TestAttributeBuilder {
160    /// Create a new builder with the given name and type. All flags default to `false`.
161    #[must_use]
162    pub fn new(name: &str, iac_type: IacType) -> Self {
163        Self {
164            api_name: name.to_string(),
165            canonical_name: name.replace('-', "_"),
166            description: String::new(),
167            iac_type,
168            required: false,
169            optional: false,
170            computed: false,
171            sensitive: false,
172            json_encoded: false,
173            immutable: false,
174            default_value: None,
175            enum_values: None,
176            read_path: None,
177            update_only: false,
178        }
179    }
180
181    /// Mark the attribute as required.
182    #[must_use]
183    pub fn required(mut self) -> Self {
184        self.required = true;
185        self
186    }
187
188    /// Mark the attribute as optional.
189    #[must_use]
190    pub fn optional(mut self) -> Self {
191        self.optional = true;
192        self
193    }
194
195    /// Mark the attribute as computed.
196    #[must_use]
197    pub fn computed(mut self) -> Self {
198        self.computed = true;
199        self
200    }
201
202    /// Mark the attribute as sensitive.
203    #[must_use]
204    pub fn sensitive(mut self) -> Self {
205        self.sensitive = true;
206        self
207    }
208
209    /// Mark the attribute as carrying JSON-encoded string content.
210    #[must_use]
211    pub fn json_encoded(mut self) -> Self {
212        self.json_encoded = true;
213        self
214    }
215
216    /// Mark the attribute as immutable (force-new on change).
217    #[must_use]
218    pub fn immutable(mut self) -> Self {
219        self.immutable = true;
220        self
221    }
222
223    /// Set the read path for mapping from API response.
224    #[must_use]
225    pub fn read_path(mut self, path: &str) -> Self {
226        self.read_path = Some(path.to_string());
227        self
228    }
229
230    /// Set the attribute description.
231    #[must_use]
232    pub fn description(mut self, desc: &str) -> Self {
233        self.description = desc.to_string();
234        self
235    }
236
237    /// Set a default value.
238    #[must_use]
239    pub fn default_value(mut self, val: serde_json::Value) -> Self {
240        self.default_value = Some(val);
241        self
242    }
243
244    /// Set enum constraint values.
245    #[must_use]
246    pub fn enum_values(mut self, values: Vec<String>) -> Self {
247        self.enum_values = Some(values);
248        self
249    }
250
251    /// Mark the attribute as update-only.
252    #[must_use]
253    pub fn update_only(mut self) -> Self {
254        self.update_only = true;
255        self
256    }
257
258    /// Build the `IacAttribute`.
259    #[must_use]
260    pub fn build(self) -> IacAttribute {
261        IacAttribute {
262            api_name: self.api_name,
263            canonical_name: self.canonical_name,
264            description: self.description,
265            iac_type: self.iac_type,
266            required: self.required,
267            optional: self.optional,
268            computed: self.computed,
269            sensitive: self.sensitive,
270            json_encoded: self.json_encoded,
271            immutable: self.immutable,
272            default_value: self.default_value,
273            enum_values: self.enum_values,
274            read_path: self.read_path,
275            update_only: self.update_only,
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::ir::HasAttributes;
284
285    #[test]
286    fn test_provider_creates_valid_provider() {
287        let p = test_provider("acme");
288        assert_eq!(p.name, "acme");
289        assert_eq!(p.description, "acme test provider");
290        assert_eq!(p.version, "1.0.0");
291        assert_eq!(p.auth.token_field, "token");
292        assert_eq!(p.auth.env_var, "ACME_TOKEN");
293        assert_eq!(p.auth.gateway_url_field, "api_url");
294        assert_eq!(p.auth.gateway_env_var, "ACME_API_URL");
295        assert_eq!(p.skip_fields, vec!["token"]);
296        assert!(p.platform_config.is_empty());
297    }
298
299    #[test]
300    fn test_resource_creates_three_attributes() {
301        let r = test_resource("secret");
302        assert_eq!(r.name, "secret");
303        assert_eq!(r.attributes.len(), 3);
304
305        let name_attr = &r.attributes[0];
306        assert_eq!(name_attr.canonical_name, "name");
307        assert!(name_attr.required);
308        assert!(name_attr.immutable);
309        assert_eq!(name_attr.read_path, Some("item_name".to_string()));
310
311        let value_attr = &r.attributes[1];
312        assert_eq!(value_attr.canonical_name, "value");
313        assert!(value_attr.required);
314        assert!(value_attr.sensitive);
315
316        let tags_attr = &r.attributes[2];
317        assert_eq!(tags_attr.canonical_name, "tags");
318        assert!(!tags_attr.required);
319        assert_eq!(tags_attr.read_path, Some("item_tags".to_string()));
320        assert_eq!(tags_attr.iac_type, IacType::List(Box::new(IacType::String)));
321    }
322
323    #[test]
324    fn test_resource_with_type_single_attribute() {
325        let r = test_resource_with_type("flag", "enabled", IacType::Boolean);
326        assert_eq!(r.attributes.len(), 1);
327        assert_eq!(r.attributes[0].canonical_name, "enabled");
328        assert_eq!(r.attributes[0].iac_type, IacType::Boolean);
329        assert!(r.attributes[0].required);
330    }
331
332    #[test]
333    fn test_data_source_creates_two_attributes() {
334        let ds = test_data_source("config");
335        assert_eq!(ds.name, "config");
336        assert_eq!(ds.attributes.len(), 2);
337
338        let name_attr = &ds.attributes[0];
339        assert_eq!(name_attr.canonical_name, "name");
340        assert!(name_attr.required);
341
342        let value_attr = &ds.attributes[1];
343        assert_eq!(value_attr.canonical_name, "value");
344        assert!(value_attr.computed);
345    }
346
347    #[test]
348    fn test_attribute_builder_defaults() {
349        let attr = TestAttributeBuilder::new("field", IacType::String).build();
350        assert_eq!(attr.api_name, "field");
351        assert_eq!(attr.canonical_name, "field");
352        assert_eq!(attr.iac_type, IacType::String);
353        assert!(!attr.required);
354        assert!(!attr.optional);
355        assert!(!attr.computed);
356        assert!(!attr.sensitive);
357        assert!(!attr.json_encoded);
358        assert!(!attr.immutable);
359        assert!(!attr.update_only);
360        assert!(attr.description.is_empty());
361        assert!(attr.default_value.is_none());
362        assert!(attr.enum_values.is_none());
363        assert!(attr.read_path.is_none());
364    }
365
366    #[test]
367    fn test_attribute_builder_all_flags() {
368        let attr = TestAttributeBuilder::new("secret-key", IacType::String)
369            .required()
370            .optional()
371            .computed()
372            .sensitive()
373            .json_encoded()
374            .immutable()
375            .update_only()
376            .read_path("secret_key_resp")
377            .description("A secret key")
378            .default_value(serde_json::json!("default"))
379            .enum_values(vec!["a".into(), "b".into()])
380            .build();
381        assert_eq!(attr.api_name, "secret-key");
382        assert_eq!(attr.canonical_name, "secret_key");
383        assert!(attr.required);
384        assert!(attr.optional);
385        assert!(attr.computed);
386        assert!(attr.sensitive);
387        assert!(attr.json_encoded);
388        assert!(attr.immutable);
389        assert!(attr.update_only);
390        assert_eq!(attr.read_path, Some("secret_key_resp".to_string()));
391        assert_eq!(attr.description, "A secret key");
392        assert_eq!(attr.default_value, Some(serde_json::json!("default")));
393        assert_eq!(
394            attr.enum_values,
395            Some(vec!["a".to_string(), "b".to_string()])
396        );
397    }
398
399    #[test]
400    fn test_resource_with_type_integer() {
401        let r = test_resource_with_type("counter", "count", IacType::Integer);
402        assert_eq!(r.attributes.len(), 1);
403        assert_eq!(r.attributes[0].iac_type, IacType::Integer);
404        assert!(r.attributes[0].required);
405    }
406
407    #[test]
408    fn test_resource_with_type_float() {
409        let r = test_resource_with_type("metric", "value", IacType::Float);
410        assert_eq!(r.attributes[0].iac_type, IacType::Float);
411    }
412
413    #[test]
414    fn test_resource_with_type_list() {
415        let r = test_resource_with_type("tagged", "tags", IacType::List(Box::new(IacType::String)));
416        assert_eq!(
417            r.attributes[0].iac_type,
418            IacType::List(Box::new(IacType::String))
419        );
420    }
421
422    #[test]
423    fn test_resource_with_type_map() {
424        let r = test_resource_with_type(
425            "config",
426            "settings",
427            IacType::Map(Box::new(IacType::String)),
428        );
429        assert_eq!(
430            r.attributes[0].iac_type,
431            IacType::Map(Box::new(IacType::String))
432        );
433    }
434
435    #[test]
436    fn test_resource_with_type_set() {
437        let r =
438            test_resource_with_type("unique", "items", IacType::Set(Box::new(IacType::Integer)));
439        assert_eq!(
440            r.attributes[0].iac_type,
441            IacType::Set(Box::new(IacType::Integer))
442        );
443    }
444
445    #[test]
446    fn test_resource_with_type_object() {
447        let r = test_resource_with_type(
448            "complex",
449            "config",
450            IacType::Object {
451                name: "Config".to_string(),
452                fields: vec![],
453            },
454        );
455        assert_eq!(
456            r.attributes[0].iac_type,
457            IacType::Object {
458                name: "Config".to_string(),
459                fields: vec![]
460            }
461        );
462    }
463
464    #[test]
465    fn test_resource_with_type_enum() {
466        let r = test_resource_with_type(
467            "status",
468            "state",
469            IacType::Enum {
470                values: vec!["active".to_string(), "inactive".to_string()],
471                underlying: Box::new(IacType::String),
472            },
473        );
474        assert_eq!(
475            r.attributes[0].iac_type,
476            IacType::Enum {
477                values: vec!["active".to_string(), "inactive".to_string()],
478                underlying: Box::new(IacType::String)
479            }
480        );
481    }
482
483    #[test]
484    fn test_resource_with_type_any() {
485        let r = test_resource_with_type("dynamic", "data", IacType::Any);
486        assert_eq!(r.attributes[0].iac_type, IacType::Any);
487    }
488
489    #[test]
490    fn test_attribute_builder_hyphenated_name() {
491        let attr = TestAttributeBuilder::new("my-field-name", IacType::String).build();
492        assert_eq!(attr.api_name, "my-field-name");
493        assert_eq!(attr.canonical_name, "my_field_name");
494    }
495
496    #[test]
497    fn test_attribute_builder_already_snake_case() {
498        let attr = TestAttributeBuilder::new("already_snake", IacType::String).build();
499        assert_eq!(attr.api_name, "already_snake");
500        assert_eq!(attr.canonical_name, "already_snake");
501    }
502
503    #[test]
504    fn test_resource_crud_endpoints() {
505        let r = test_resource("widget");
506        assert_eq!(r.crud.create_endpoint, "/create-widget");
507        assert_eq!(r.crud.read_endpoint, "/get-widget");
508        assert_eq!(r.crud.delete_endpoint, "/delete-widget");
509        assert!(r.crud.update_endpoint.is_none());
510        assert!(r.crud.update_schema.is_none());
511    }
512
513    #[test]
514    fn test_data_source_read_info() {
515        let ds = test_data_source("info");
516        assert_eq!(ds.read_endpoint, "/get-info");
517        assert_eq!(ds.read_schema, "Getinfo");
518        assert!(ds.read_response_schema.is_none());
519    }
520
521    #[test]
522    fn test_resource_with_type_identity() {
523        let r = test_resource_with_type("item", "my_id", IacType::String);
524        assert_eq!(r.identity.id_field, "my_id");
525        assert_eq!(r.identity.import_field, "my_id");
526        assert!(r.identity.force_replace_fields.is_empty());
527    }
528
529    #[test]
530    fn test_attribute_builder_multiple_hyphens() {
531        let attr = TestAttributeBuilder::new("a-b-c-d", IacType::String).build();
532        assert_eq!(attr.api_name, "a-b-c-d");
533        assert_eq!(attr.canonical_name, "a_b_c_d");
534    }
535
536    #[test]
537    fn test_attribute_builder_json_encoded_flag() {
538        let attr = TestAttributeBuilder::new("policy", IacType::String)
539            .json_encoded()
540            .build();
541        assert!(attr.json_encoded);
542    }
543
544    #[test]
545    fn test_attribute_builder_default_value_types() {
546        let bool_attr = TestAttributeBuilder::new("flag", IacType::Boolean)
547            .default_value(serde_json::json!(true))
548            .build();
549        assert_eq!(bool_attr.default_value, Some(serde_json::json!(true)));
550
551        let num_attr = TestAttributeBuilder::new("count", IacType::Integer)
552            .default_value(serde_json::json!(42))
553            .build();
554        assert_eq!(num_attr.default_value, Some(serde_json::json!(42)));
555
556        let null_attr = TestAttributeBuilder::new("maybe", IacType::String)
557            .default_value(serde_json::json!(null))
558            .build();
559        assert_eq!(null_attr.default_value, Some(serde_json::json!(null)));
560    }
561
562    #[test]
563    fn test_attribute_builder_enum_values_empty() {
564        let attr = TestAttributeBuilder::new("mode", IacType::String)
565            .enum_values(vec![])
566            .build();
567        assert_eq!(attr.enum_values, Some(vec![]));
568    }
569
570    #[test]
571    fn test_resource_identity() {
572        let r = test_resource("secret");
573        assert_eq!(r.identity.id_field, "name");
574        assert_eq!(r.identity.import_field, "name");
575        assert_eq!(r.identity.force_replace_fields, vec!["name"]);
576    }
577
578    #[test]
579    fn test_resource_description() {
580        let r = test_resource("widget");
581        assert_eq!(r.description, "widget test resource");
582        assert_eq!(r.category, "test");
583    }
584
585    #[test]
586    fn test_data_source_description() {
587        let ds = test_data_source("lookup");
588        assert_eq!(ds.description, "lookup test data source");
589    }
590
591    #[test]
592    fn test_resource_with_type_numeric() {
593        let r = test_resource_with_type("measure", "amount", IacType::Numeric);
594        assert_eq!(r.attributes[0].iac_type, IacType::Numeric);
595    }
596
597    #[test]
598    fn test_provider_auth_has_helpers() {
599        let p = test_provider("demo");
600        assert!(p.auth.has_token());
601        assert!(p.auth.has_gateway());
602    }
603
604    #[test]
605    fn test_resource_filter_helpers_via_fixture() {
606        let r = test_resource("secret");
607        let required = r.required_attribute_names();
608        assert_eq!(required, vec!["name", "value"]);
609
610        let sensitive = r.sensitive_attribute_names();
611        assert_eq!(sensitive, vec!["value"]);
612
613        let immutable = r.immutable_attribute_names();
614        assert_eq!(immutable, vec!["name"]);
615    }
616
617    #[test]
618    fn test_data_source_filter_helpers_via_fixture() {
619        let ds = test_data_source("config");
620        let required = ds.required_attribute_names();
621        assert_eq!(required, vec!["name"]);
622
623        let computed = ds.computed_attribute_names();
624        assert_eq!(computed, vec!["value"]);
625
626        let sensitive = ds.sensitive_attribute_names();
627        assert!(sensitive.is_empty());
628    }
629}