1use crate::error::ProviderError;
29use crate::schema::{Diagnostic, DiagnosticSeverity, ProviderSchema};
30use crate::server::ProviderService;
31use crate::types::{ImportedResource, PlanResult};
32use serde_json::Value;
33
34pub struct ProviderTester<P: ProviderService> {
49 provider: P,
50}
51
52impl<P: ProviderService> ProviderTester<P> {
53 pub fn new(provider: P) -> Self {
55 Self { provider }
56 }
57
58 pub fn provider(&self) -> &P {
60 &self.provider
61 }
62
63 pub fn provider_mut(&mut self) -> &mut P {
65 &mut self.provider
66 }
67
68 pub fn schema(&self) -> ProviderSchema {
74 self.provider.schema()
75 }
76
77 pub fn resource_types(&self) -> Vec<String> {
79 self.provider.metadata().resources
80 }
81
82 pub fn data_source_types(&self) -> Vec<String> {
84 self.provider.metadata().data_sources
85 }
86
87 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 pub async fn configure(&self, config: Value) -> Result<(), TestError> {
105 let diagnostics = self.provider.configure(config).await?;
106 check_diagnostics(diagnostics)
107 }
108
109 pub async fn stop(&self) -> Result<(), ProviderError> {
111 self.provider.stop().await
112 }
113
114 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn lifecycle_create(
279 &self,
280 resource_type: &str,
281 config: Value,
282 ) -> Result<Value, ProviderError> {
283 let plan_result = self.plan_create(resource_type, config).await?;
285
286 let created_state = self
288 .create(resource_type, plan_result.planned_state)
289 .await?;
290
291 self.read(resource_type, created_state).await
293 }
294
295 pub async fn lifecycle_update(
299 &self,
300 resource_type: &str,
301 prior_state: Value,
302 proposed_state: Value,
303 ) -> Result<Value, ProviderError> {
304 let plan_result = self
306 .plan_update(resource_type, prior_state.clone(), proposed_state)
307 .await?;
308
309 let updated_state = self
311 .update(resource_type, prior_state, plan_result.planned_state)
312 .await?;
313
314 self.read(resource_type, updated_state).await
316 }
317
318 pub async fn lifecycle_delete(
320 &self,
321 resource_type: &str,
322 current_state: Value,
323 ) -> Result<(), ProviderError> {
324 let _ = self
326 .plan_delete(resource_type, current_state.clone())
327 .await?;
328
329 self.delete(resource_type, current_state).await
331 }
332
333 pub async fn lifecycle_crud(
337 &self,
338 resource_type: &str,
339 initial_config: Value,
340 updated_config: Value,
341 ) -> Result<Value, ProviderError> {
342 let created_state = self.lifecycle_create(resource_type, initial_config).await?;
344
345 let updated_state = self
347 .lifecycle_update(resource_type, created_state.clone(), updated_config)
348 .await?;
349
350 self.lifecycle_delete(resource_type, updated_state.clone())
352 .await?;
353
354 Ok(updated_state)
355 }
356}
357
358#[derive(Debug)]
360pub enum TestError {
361 Diagnostics(Vec<Diagnostic>),
363 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
397fn 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
411pub 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
431pub 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
445pub 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
457pub 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
469pub 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
481pub 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
496pub 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
510pub 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
529pub 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
542pub 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 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 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 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}