hemmer_provider_sdk/
testing.rs

1//! Testing utilities for provider implementations.
2//!
3//! This module provides utilities to test `ProviderService` implementations
4//! without spinning up a full gRPC server.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use hemmer_provider_sdk::testing::ProviderTester;
10//! use serde_json::json;
11//!
12//! #[tokio::test]
13//! async fn test_create_resource() {
14//!     let tester = ProviderTester::new(MyProvider::new());
15//!
16//!     // Configure the provider
17//!     tester.configure(json!({"api_key": "test"})).await.unwrap();
18//!
19//!     // Test create
20//!     let state = tester.create("my_resource", json!({
21//!         "name": "test-resource"
22//!     })).await.unwrap();
23//!
24//!     assert_eq!(state["name"], "test-resource");
25//! }
26//! ```
27
28use crate::error::ProviderError;
29use crate::schema::{Diagnostic, DiagnosticSeverity, ProviderSchema};
30use crate::server::ProviderService;
31use crate::types::{ImportedResource, PlanResult};
32use serde_json::Value;
33
34/// A test harness for provider implementations.
35///
36/// This wraps a `ProviderService` implementation and provides
37/// simplified methods for testing without a gRPC server.
38///
39/// # Example
40///
41/// ```ignore
42/// use hemmer_provider_sdk::testing::ProviderTester;
43///
44/// let tester = ProviderTester::new(MyProvider::new());
45/// tester.configure(json!({})).await.unwrap();
46/// let state = tester.create("my_resource", json!({"name": "test"})).await.unwrap();
47/// ```
48pub struct ProviderTester<P: ProviderService> {
49    provider: P,
50}
51
52impl<P: ProviderService> ProviderTester<P> {
53    /// Create a new tester for the given provider.
54    pub fn new(provider: P) -> Self {
55        Self { provider }
56    }
57
58    /// Get a reference to the underlying provider.
59    pub fn provider(&self) -> &P {
60        &self.provider
61    }
62
63    /// Get a mutable reference to the underlying provider.
64    pub fn provider_mut(&mut self) -> &mut P {
65        &mut self.provider
66    }
67
68    // =========================================================================
69    // Schema & Metadata
70    // =========================================================================
71
72    /// Get the provider's schema.
73    pub fn schema(&self) -> ProviderSchema {
74        self.provider.schema()
75    }
76
77    /// Get the list of resource type names.
78    pub fn resource_types(&self) -> Vec<String> {
79        self.provider.metadata().resources
80    }
81
82    /// Get the list of data source type names.
83    pub fn data_source_types(&self) -> Vec<String> {
84        self.provider.metadata().data_sources
85    }
86
87    // =========================================================================
88    // Provider Lifecycle
89    // =========================================================================
90
91    /// Validate provider configuration.
92    ///
93    /// Returns `Ok(())` if validation passes (no error diagnostics).
94    /// Returns `Err` with the diagnostics if there are errors.
95    pub async fn validate_provider_config(&self, config: Value) -> Result<(), TestError> {
96        let diagnostics = self.provider.validate_provider_config(config).await?;
97        check_diagnostics(diagnostics)
98    }
99
100    /// Configure the provider.
101    ///
102    /// Returns `Ok(())` if configuration succeeds.
103    /// Returns `Err` with the diagnostics if there are errors.
104    pub async fn configure(&self, config: Value) -> Result<(), TestError> {
105        let diagnostics = self.provider.configure(config).await?;
106        check_diagnostics(diagnostics)
107    }
108
109    /// Stop the provider.
110    pub async fn stop(&self) -> Result<(), ProviderError> {
111        self.provider.stop().await
112    }
113
114    // =========================================================================
115    // Resource Operations
116    // =========================================================================
117
118    /// Validate a resource configuration.
119    pub async fn validate_resource_config(
120        &self,
121        resource_type: &str,
122        config: Value,
123    ) -> Result<(), TestError> {
124        let diagnostics = self
125            .provider
126            .validate_resource_config(resource_type, config)
127            .await?;
128        check_diagnostics(diagnostics)
129    }
130
131    /// Plan a resource creation (no prior state).
132    pub async fn plan_create(
133        &self,
134        resource_type: &str,
135        proposed_state: Value,
136    ) -> Result<PlanResult, ProviderError> {
137        self.provider
138            .plan(resource_type, None, proposed_state.clone(), proposed_state)
139            .await
140    }
141
142    /// Plan a resource update.
143    pub async fn plan_update(
144        &self,
145        resource_type: &str,
146        prior_state: Value,
147        proposed_state: Value,
148    ) -> Result<PlanResult, ProviderError> {
149        self.provider
150            .plan(
151                resource_type,
152                Some(prior_state),
153                proposed_state.clone(),
154                proposed_state,
155            )
156            .await
157    }
158
159    /// Plan a resource deletion.
160    pub async fn plan_delete(
161        &self,
162        resource_type: &str,
163        prior_state: Value,
164    ) -> Result<PlanResult, ProviderError> {
165        self.provider
166            .plan(resource_type, Some(prior_state), Value::Null, Value::Null)
167            .await
168    }
169
170    /// Full plan operation with explicit config.
171    pub async fn plan(
172        &self,
173        resource_type: &str,
174        prior_state: Option<Value>,
175        proposed_state: Value,
176        config: Value,
177    ) -> Result<PlanResult, ProviderError> {
178        self.provider
179            .plan(resource_type, prior_state, proposed_state, config)
180            .await
181    }
182
183    /// Create a new resource.
184    pub async fn create(
185        &self,
186        resource_type: &str,
187        planned_state: Value,
188    ) -> Result<Value, ProviderError> {
189        self.provider.create(resource_type, planned_state).await
190    }
191
192    /// Read the current state of a resource.
193    pub async fn read(
194        &self,
195        resource_type: &str,
196        current_state: Value,
197    ) -> Result<Value, ProviderError> {
198        self.provider.read(resource_type, current_state).await
199    }
200
201    /// Update an existing resource.
202    pub async fn update(
203        &self,
204        resource_type: &str,
205        prior_state: Value,
206        planned_state: Value,
207    ) -> Result<Value, ProviderError> {
208        self.provider
209            .update(resource_type, prior_state, planned_state)
210            .await
211    }
212
213    /// Delete a resource.
214    pub async fn delete(
215        &self,
216        resource_type: &str,
217        current_state: Value,
218    ) -> Result<(), ProviderError> {
219        self.provider.delete(resource_type, current_state).await
220    }
221
222    /// Import an existing resource.
223    pub async fn import_resource(
224        &self,
225        resource_type: &str,
226        id: &str,
227    ) -> Result<Vec<ImportedResource>, ProviderError> {
228        self.provider.import_resource(resource_type, id).await
229    }
230
231    /// Upgrade resource state from an older schema version.
232    pub async fn upgrade_resource_state(
233        &self,
234        resource_type: &str,
235        version: i64,
236        state: Value,
237    ) -> Result<Value, ProviderError> {
238        self.provider
239            .upgrade_resource_state(resource_type, version, state)
240            .await
241    }
242
243    // =========================================================================
244    // Data Source Operations
245    // =========================================================================
246
247    /// Validate a data source configuration.
248    pub async fn validate_data_source_config(
249        &self,
250        data_source_type: &str,
251        config: Value,
252    ) -> Result<(), TestError> {
253        let diagnostics = self
254            .provider
255            .validate_data_source_config(data_source_type, config)
256            .await?;
257        check_diagnostics(diagnostics)
258    }
259
260    /// Read data from a data source.
261    pub async fn read_data_source(
262        &self,
263        data_source_type: &str,
264        config: Value,
265    ) -> Result<Value, ProviderError> {
266        self.provider
267            .read_data_source(data_source_type, config)
268            .await
269    }
270
271    // =========================================================================
272    // Lifecycle Helpers
273    // =========================================================================
274
275    /// Run a full create lifecycle: plan → create → read.
276    ///
277    /// Returns the final state after read.
278    pub async fn lifecycle_create(
279        &self,
280        resource_type: &str,
281        config: Value,
282    ) -> Result<Value, ProviderError> {
283        // Plan
284        let plan_result = self.plan_create(resource_type, config).await?;
285
286        // Create
287        let created_state = self
288            .create(resource_type, plan_result.planned_state)
289            .await?;
290
291        // Read to verify
292        self.read(resource_type, created_state).await
293    }
294
295    /// Run a full update lifecycle: plan → update → read.
296    ///
297    /// Returns the final state after read.
298    pub async fn lifecycle_update(
299        &self,
300        resource_type: &str,
301        prior_state: Value,
302        proposed_state: Value,
303    ) -> Result<Value, ProviderError> {
304        // Plan
305        let plan_result = self
306            .plan_update(resource_type, prior_state.clone(), proposed_state)
307            .await?;
308
309        // Update
310        let updated_state = self
311            .update(resource_type, prior_state, plan_result.planned_state)
312            .await?;
313
314        // Read to verify
315        self.read(resource_type, updated_state).await
316    }
317
318    /// Run a full delete lifecycle: plan → delete.
319    pub async fn lifecycle_delete(
320        &self,
321        resource_type: &str,
322        current_state: Value,
323    ) -> Result<(), ProviderError> {
324        // Plan (optional, but good practice)
325        let _ = self
326            .plan_delete(resource_type, current_state.clone())
327            .await?;
328
329        // Delete
330        self.delete(resource_type, current_state).await
331    }
332
333    /// Run a full CRUD lifecycle: create → read → update → read → delete.
334    ///
335    /// Returns the state after the update (before delete).
336    pub async fn lifecycle_crud(
337        &self,
338        resource_type: &str,
339        initial_config: Value,
340        updated_config: Value,
341    ) -> Result<Value, ProviderError> {
342        // Create
343        let created_state = self.lifecycle_create(resource_type, initial_config).await?;
344
345        // Update
346        let updated_state = self
347            .lifecycle_update(resource_type, created_state.clone(), updated_config)
348            .await?;
349
350        // Delete
351        self.lifecycle_delete(resource_type, updated_state.clone())
352            .await?;
353
354        Ok(updated_state)
355    }
356}
357
358/// Error type for test operations that may fail with diagnostics.
359#[derive(Debug)]
360pub enum TestError {
361    /// The operation failed with diagnostics.
362    Diagnostics(Vec<Diagnostic>),
363    /// The operation failed with a provider error.
364    Provider(ProviderError),
365}
366
367impl std::fmt::Display for TestError {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        match self {
370            TestError::Diagnostics(diags) => {
371                writeln!(f, "Operation failed with {} diagnostic(s):", diags.len())?;
372                for diag in diags {
373                    write!(f, "  [{:?}] {}", diag.severity, diag.summary)?;
374                    if let Some(detail) = &diag.detail {
375                        write!(f, ": {}", detail)?;
376                    }
377                    if let Some(attr) = &diag.attribute {
378                        write!(f, " (at {})", attr)?;
379                    }
380                    writeln!(f)?;
381                }
382                Ok(())
383            },
384            TestError::Provider(e) => write!(f, "Provider error: {}", e),
385        }
386    }
387}
388
389impl std::error::Error for TestError {}
390
391impl From<ProviderError> for TestError {
392    fn from(e: ProviderError) -> Self {
393        TestError::Provider(e)
394    }
395}
396
397/// Check diagnostics and return an error if there are any errors.
398fn check_diagnostics(diagnostics: Vec<Diagnostic>) -> Result<(), TestError> {
399    let errors: Vec<_> = diagnostics
400        .into_iter()
401        .filter(|d| matches!(d.severity, DiagnosticSeverity::Error))
402        .collect();
403
404    if errors.is_empty() {
405        Ok(())
406    } else {
407        Err(TestError::Diagnostics(errors))
408    }
409}
410
411// =========================================================================
412// Assertion Helpers
413// =========================================================================
414
415/// Assert that a plan result indicates the resource will be created.
416///
417/// # Panics
418///
419/// Panics if the plan has no changes or requires replacement.
420pub fn assert_plan_creates(plan: &PlanResult) {
421    assert!(
422        !plan.changes.is_empty(),
423        "Expected plan to have changes for create, but got no changes"
424    );
425    assert!(
426        !plan.requires_replace,
427        "Expected plan to create, not replace"
428    );
429}
430
431/// Assert that a plan result indicates no changes.
432///
433/// # Panics
434///
435/// Panics if the plan has any changes.
436pub fn assert_plan_no_changes(plan: &PlanResult) {
437    assert!(
438        plan.changes.is_empty(),
439        "Expected no changes, but got {} change(s): {:?}",
440        plan.changes.len(),
441        plan.changes.iter().map(|c| &c.path).collect::<Vec<_>>()
442    );
443}
444
445/// Assert that a plan result indicates changes are needed.
446///
447/// # Panics
448///
449/// Panics if the plan has no changes.
450pub fn assert_plan_has_changes(plan: &PlanResult) {
451    assert!(
452        !plan.changes.is_empty(),
453        "Expected plan to have changes, but got no changes"
454    );
455}
456
457/// Assert that a plan requires resource replacement.
458///
459/// # Panics
460///
461/// Panics if the plan does not require replacement.
462pub fn assert_plan_replaces(plan: &PlanResult) {
463    assert!(
464        plan.requires_replace,
465        "Expected plan to require replacement, but it does not"
466    );
467}
468
469/// Assert that a plan does not require resource replacement.
470///
471/// # Panics
472///
473/// Panics if the plan requires replacement.
474pub fn assert_plan_updates_in_place(plan: &PlanResult) {
475    assert!(
476        !plan.requires_replace,
477        "Expected plan to update in place, but it requires replacement"
478    );
479}
480
481/// Assert that a plan has a change for a specific attribute path.
482///
483/// # Panics
484///
485/// Panics if the plan does not have a change for the given path.
486pub fn assert_plan_changes_attribute(plan: &PlanResult, path: &str) {
487    let has_change = plan.changes.iter().any(|c| c.path == path);
488    assert!(
489        has_change,
490        "Expected plan to change attribute '{}', but it was not changed. Changed attributes: {:?}",
491        path,
492        plan.changes.iter().map(|c| &c.path).collect::<Vec<_>>()
493    );
494}
495
496/// Assert that a plan does not have a change for a specific attribute path.
497///
498/// # Panics
499///
500/// Panics if the plan has a change for the given path.
501pub fn assert_plan_does_not_change_attribute(plan: &PlanResult, path: &str) {
502    let has_change = plan.changes.iter().any(|c| c.path == path);
503    assert!(
504        !has_change,
505        "Expected plan to not change attribute '{}', but it was changed",
506        path
507    );
508}
509
510/// Assert that diagnostics contain no errors.
511///
512/// # Panics
513///
514/// Panics if there are any error diagnostics.
515pub fn assert_no_errors(diagnostics: &[Diagnostic]) {
516    let errors: Vec<_> = diagnostics
517        .iter()
518        .filter(|d| matches!(d.severity, DiagnosticSeverity::Error))
519        .collect();
520
521    assert!(
522        errors.is_empty(),
523        "Expected no errors, but got {} error(s): {:?}",
524        errors.len(),
525        errors.iter().map(|d| &d.summary).collect::<Vec<_>>()
526    );
527}
528
529/// Assert that diagnostics contain at least one error.
530///
531/// # Panics
532///
533/// Panics if there are no error diagnostics.
534pub fn assert_has_errors(diagnostics: &[Diagnostic]) {
535    let has_errors = diagnostics
536        .iter()
537        .any(|d| matches!(d.severity, DiagnosticSeverity::Error));
538
539    assert!(has_errors, "Expected at least one error, but got none");
540}
541
542/// Assert that diagnostics contain an error with the given summary substring.
543///
544/// # Panics
545///
546/// Panics if no error diagnostic contains the given substring.
547pub fn assert_error_contains(diagnostics: &[Diagnostic], substring: &str) {
548    let has_matching_error = diagnostics
549        .iter()
550        .any(|d| matches!(d.severity, DiagnosticSeverity::Error) && d.summary.contains(substring));
551
552    assert!(
553        has_matching_error,
554        "Expected an error containing '{}', but no matching error found. Errors: {:?}",
555        substring,
556        diagnostics
557            .iter()
558            .filter(|d| matches!(d.severity, DiagnosticSeverity::Error))
559            .map(|d| &d.summary)
560            .collect::<Vec<_>>()
561    );
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use crate::schema::{Attribute, Schema};
568    use crate::types::AttributeChange;
569    use serde_json::json;
570
571    // A simple test provider for testing the tester
572    struct TestProvider;
573
574    #[async_trait::async_trait]
575    impl ProviderService for TestProvider {
576        fn schema(&self) -> ProviderSchema {
577            ProviderSchema::new()
578                .with_provider_config(
579                    Schema::v0().with_attribute("api_key", Attribute::optional_string()),
580                )
581                .with_resource(
582                    "test_resource",
583                    Schema::v0()
584                        .with_attribute("name", Attribute::required_string())
585                        .with_attribute("id", Attribute::computed_string()),
586                )
587        }
588
589        async fn configure(&self, _config: Value) -> Result<Vec<Diagnostic>, ProviderError> {
590            Ok(vec![])
591        }
592
593        async fn plan(
594            &self,
595            _resource_type: &str,
596            prior_state: Option<Value>,
597            proposed_state: Value,
598            _config: Value,
599        ) -> Result<PlanResult, ProviderError> {
600            match prior_state {
601                None => {
602                    // Create
603                    let mut planned = proposed_state.clone();
604                    if let Value::Object(ref mut map) = planned {
605                        map.insert("id".to_string(), json!("generated-id"));
606                    }
607                    Ok(PlanResult::with_changes(
608                        planned,
609                        vec![AttributeChange::added("id", json!("generated-id"))],
610                        false,
611                    ))
612                },
613                Some(prior) => {
614                    // Update - check if name changed
615                    if prior.get("name") != proposed_state.get("name") {
616                        let mut planned = proposed_state.clone();
617                        if let Value::Object(ref mut map) = planned {
618                            map.insert("id".to_string(), prior["id"].clone());
619                        }
620                        Ok(PlanResult::with_changes(
621                            planned,
622                            vec![AttributeChange::modified(
623                                "name",
624                                prior["name"].clone(),
625                                proposed_state["name"].clone(),
626                            )],
627                            false,
628                        ))
629                    } else {
630                        Ok(PlanResult::no_change(prior))
631                    }
632                },
633            }
634        }
635
636        async fn create(
637            &self,
638            _resource_type: &str,
639            planned_state: Value,
640        ) -> Result<Value, ProviderError> {
641            Ok(planned_state)
642        }
643
644        async fn read(
645            &self,
646            _resource_type: &str,
647            current_state: Value,
648        ) -> Result<Value, ProviderError> {
649            Ok(current_state)
650        }
651
652        async fn update(
653            &self,
654            _resource_type: &str,
655            _prior_state: Value,
656            planned_state: Value,
657        ) -> Result<Value, ProviderError> {
658            Ok(planned_state)
659        }
660
661        async fn delete(
662            &self,
663            _resource_type: &str,
664            _current_state: Value,
665        ) -> Result<(), ProviderError> {
666            Ok(())
667        }
668    }
669
670    #[tokio::test]
671    async fn test_tester_configure() {
672        let tester = ProviderTester::new(TestProvider);
673        let result = tester.configure(json!({"api_key": "test"})).await;
674        assert!(result.is_ok());
675    }
676
677    #[tokio::test]
678    async fn test_tester_schema() {
679        let tester = ProviderTester::new(TestProvider);
680        let schema = tester.schema();
681        assert!(schema.resources.contains_key("test_resource"));
682    }
683
684    #[tokio::test]
685    async fn test_tester_resource_types() {
686        let tester = ProviderTester::new(TestProvider);
687        let types = tester.resource_types();
688        assert!(types.contains(&"test_resource".to_string()));
689    }
690
691    #[tokio::test]
692    async fn test_tester_plan_create() {
693        let tester = ProviderTester::new(TestProvider);
694        let plan = tester
695            .plan_create("test_resource", json!({"name": "test"}))
696            .await
697            .unwrap();
698
699        assert_plan_creates(&plan);
700        assert_eq!(plan.planned_state["id"], "generated-id");
701    }
702
703    #[tokio::test]
704    async fn test_tester_plan_update_with_changes() {
705        let tester = ProviderTester::new(TestProvider);
706        let plan = tester
707            .plan_update(
708                "test_resource",
709                json!({"name": "old", "id": "123"}),
710                json!({"name": "new", "id": "123"}),
711            )
712            .await
713            .unwrap();
714
715        assert_plan_has_changes(&plan);
716        assert_plan_changes_attribute(&plan, "name");
717        assert_plan_updates_in_place(&plan);
718    }
719
720    #[tokio::test]
721    async fn test_tester_plan_update_no_changes() {
722        let tester = ProviderTester::new(TestProvider);
723        let state = json!({"name": "same", "id": "123"});
724        let plan = tester
725            .plan_update("test_resource", state.clone(), state)
726            .await
727            .unwrap();
728
729        assert_plan_no_changes(&plan);
730    }
731
732    #[tokio::test]
733    async fn test_tester_lifecycle_create() {
734        let tester = ProviderTester::new(TestProvider);
735        let state = tester
736            .lifecycle_create("test_resource", json!({"name": "test"}))
737            .await
738            .unwrap();
739
740        assert_eq!(state["name"], "test");
741        assert_eq!(state["id"], "generated-id");
742    }
743
744    #[tokio::test]
745    async fn test_tester_lifecycle_crud() {
746        let tester = ProviderTester::new(TestProvider);
747        let final_state = tester
748            .lifecycle_crud(
749                "test_resource",
750                json!({"name": "initial"}),
751                json!({"name": "updated"}),
752            )
753            .await
754            .unwrap();
755
756        assert_eq!(final_state["name"], "updated");
757    }
758
759    #[test]
760    fn test_assert_no_errors() {
761        let diagnostics = vec![Diagnostic::warning("Just a warning")];
762        assert_no_errors(&diagnostics);
763    }
764
765    #[test]
766    #[should_panic(expected = "Expected no errors")]
767    fn test_assert_no_errors_fails() {
768        let diagnostics = vec![Diagnostic::error("An error")];
769        assert_no_errors(&diagnostics);
770    }
771
772    #[test]
773    fn test_assert_has_errors() {
774        let diagnostics = vec![Diagnostic::error("An error")];
775        assert_has_errors(&diagnostics);
776    }
777
778    #[test]
779    fn test_assert_error_contains() {
780        let diagnostics = vec![Diagnostic::error("Invalid configuration value")];
781        assert_error_contains(&diagnostics, "Invalid");
782        assert_error_contains(&diagnostics, "configuration");
783    }
784
785    #[test]
786    fn test_test_error_display() {
787        let err = TestError::Diagnostics(vec![
788            Diagnostic::error("First error").with_attribute("field1"),
789            Diagnostic::error("Second error").with_detail("More info"),
790        ]);
791
792        let display = format!("{}", err);
793        assert!(display.contains("First error"));
794        assert!(display.contains("Second error"));
795        assert!(display.contains("field1"));
796        assert!(display.contains("More info"));
797    }
798}