1use std::fmt;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug)]
8pub enum ProjectManagerError {
9 FileSystem {
11 operation: String,
12 path: PathBuf,
13 source: std::io::Error,
14 },
15
16 Serialization {
18 operation: String,
19 content: String,
20 source: serde_json::Error,
21 },
22
23 Validation {
25 field: String,
26 value: String,
27 reason: String,
28 },
29
30 NotFound {
32 resource_type: String,
33 identifier: String,
34 context: Option<String>,
35 },
36
37 AlreadyExists {
39 resource_type: String,
40 identifier: String,
41 context: Option<String>,
42 },
43
44 McpProtocol {
46 operation: String,
47 details: String,
48 source: Option<Box<dyn std::error::Error + Send + Sync>>,
49 },
50
51 Configuration {
53 setting: String,
54 value: String,
55 reason: String,
56 },
57
58 Permission {
60 operation: String,
61 path: PathBuf,
62 details: String,
63 },
64
65 Internal {
67 operation: String,
68 details: String,
69 source: Option<Box<dyn std::error::Error + Send + Sync>>,
70 },
71}
72
73impl ProjectManagerError {
74 pub fn file_system_error(operation: &str, path: &Path, source: std::io::Error) -> Self {
76 Self::FileSystem {
77 operation: operation.to_string(),
78 path: path.to_path_buf(),
79 source,
80 }
81 }
82
83 pub fn serialization_error(operation: &str, content: &str, source: serde_json::Error) -> Self {
85 Self::Serialization {
86 operation: operation.to_string(),
87 content: content.to_string(),
88 source,
89 }
90 }
91
92 pub fn validation_error(field: &str, value: &str, reason: &str) -> Self {
94 Self::Validation {
95 field: field.to_string(),
96 value: value.to_string(),
97 reason: reason.to_string(),
98 }
99 }
100
101 pub fn not_found(resource_type: &str, identifier: &str, context: Option<&str>) -> Self {
103 Self::NotFound {
104 resource_type: resource_type.to_string(),
105 identifier: identifier.to_string(),
106 context: context.map(|s| s.to_string()),
107 }
108 }
109
110 pub fn already_exists(resource_type: &str, identifier: &str, context: Option<&str>) -> Self {
112 Self::AlreadyExists {
113 resource_type: resource_type.to_string(),
114 identifier: identifier.to_string(),
115 context: context.map(|s| s.to_string()),
116 }
117 }
118
119 pub fn mcp_protocol_error(
121 operation: &str,
122 details: &str,
123 source: Option<Box<dyn std::error::Error + Send + Sync>>,
124 ) -> Self {
125 Self::McpProtocol {
126 operation: operation.to_string(),
127 details: details.to_string(),
128 source,
129 }
130 }
131
132 pub fn configuration_error(setting: &str, value: &str, reason: &str) -> Self {
134 Self::Configuration {
135 setting: setting.to_string(),
136 value: value.to_string(),
137 reason: reason.to_string(),
138 }
139 }
140
141 pub fn permission_error(operation: &str, path: &Path, details: &str) -> Self {
143 Self::Permission {
144 operation: operation.to_string(),
145 path: path.to_path_buf(),
146 details: details.to_string(),
147 }
148 }
149
150 pub fn internal_error(
152 operation: &str,
153 details: &str,
154 source: Option<Box<dyn std::error::Error + Send + Sync>>,
155 ) -> Self {
156 Self::Internal {
157 operation: operation.to_string(),
158 details: details.to_string(),
159 source,
160 }
161 }
162
163 pub fn user_message(&self) -> String {
165 match self {
166 Self::FileSystem {
167 operation, path, source
168 } => {
169 let base_msg = format!("Failed to {} at path '{}'", operation, path.display());
170 if let Some(suggestion) = self.get_suggestion() {
171 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
172 } else {
173 format!("{}\nReason: {}", base_msg, source)
174 }
175 }
176 Self::Serialization { operation, .. } => {
177 let base_msg = format!("Failed to {} data", operation);
178 if let Some(suggestion) = self.get_suggestion() {
179 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
180 } else {
181 base_msg
182 }
183 }
184 Self::Validation {
185 field,
186 value,
187 reason,
188 } => {
189 let base_msg = format!(
190 "Invalid value '{}' for field '{}': {}",
191 value, field, reason
192 );
193 if let Some(suggestion) = self.get_suggestion() {
194 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
195 } else {
196 base_msg
197 }
198 }
199 Self::NotFound {
200 resource_type,
201 identifier,
202 context,
203 } => {
204 let base_msg = if let Some(ctx) = context {
205 format!("{} '{}' not found in {}", resource_type, identifier, ctx)
206 } else {
207 format!("{} '{}' not found", resource_type, identifier)
208 };
209 if let Some(suggestion) = self.get_suggestion() {
210 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
211 } else {
212 base_msg
213 }
214 }
215 Self::AlreadyExists {
216 resource_type,
217 identifier,
218 context,
219 } => {
220 let base_msg = if let Some(ctx) = context {
221 format!(
222 "{} '{}' already exists in {}",
223 resource_type, identifier, ctx
224 )
225 } else {
226 format!("{} '{}' already exists", resource_type, identifier)
227 };
228 if let Some(suggestion) = self.get_suggestion() {
229 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
230 } else {
231 base_msg
232 }
233 }
234 Self::McpProtocol {
235 operation, details, ..
236 } => {
237 let base_msg = format!("MCP protocol error during {}: {}", operation, details);
238 if let Some(suggestion) = self.get_suggestion() {
239 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
240 } else {
241 base_msg
242 }
243 }
244 Self::Configuration {
245 setting,
246 value,
247 reason,
248 } => {
249 let base_msg = format!(
250 "Configuration error for '{}' (value: '{}'): {}",
251 setting, value, reason
252 );
253 if let Some(suggestion) = self.get_suggestion() {
254 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
255 } else {
256 base_msg
257 }
258 }
259 Self::Permission {
260 operation, path, details
261 } => {
262 let base_msg = format!(
263 "Permission denied for {} at path '{}': {}",
264 operation,
265 path.display(),
266 details
267 );
268 if let Some(suggestion) = self.get_suggestion() {
269 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
270 } else {
271 base_msg
272 }
273 }
274 Self::Internal {
275 operation, details, ..
276 } => {
277 let base_msg = format!("Internal error during {}: {}", operation, details);
278 if let Some(suggestion) = self.get_suggestion() {
279 format!("{}\n\nSuggestion: {}", base_msg, suggestion)
280 } else {
281 base_msg
282 }
283 }
284 }
285 }
286
287 pub fn get_suggestion(&self) -> Option<String> {
289 match self {
290 Self::FileSystem { operation, path, source } => {
291 match source.kind() {
292 std::io::ErrorKind::NotFound => {
293 if operation.contains("create") {
294 Some("Check that the parent directory exists and you have write permissions.".to_string())
295 } else if operation.contains("read") || operation.contains("open") {
296 Some(format!("Verify that the file '{}' exists and you have read permissions.", path.display()))
297 } else {
298 Some("Check that the file or directory exists and you have the necessary permissions.".to_string())
299 }
300 }
301 std::io::ErrorKind::PermissionDenied => {
302 Some("Check file/directory permissions or run with appropriate privileges.".to_string())
303 }
304 std::io::ErrorKind::AlreadyExists => {
305 Some("Choose a different name or remove the existing file/directory first.".to_string())
306 }
307 std::io::ErrorKind::InvalidData => {
308 Some("The file may be corrupted or in an unexpected format. Try recreating it.".to_string())
309 }
310 _ => Some("Check file system permissions and available disk space.".to_string())
311 }
312 }
313 Self::Serialization { operation, .. } => {
314 if operation.contains("serialize") {
315 Some("Check that all required fields are properly set and contain valid data.".to_string())
316 } else if operation.contains("parse") || operation.contains("deserialize") {
317 Some("The file may be corrupted or in an unexpected format. Try regenerating it or check for syntax errors.".to_string())
318 } else {
319 Some("Verify that the data structure matches the expected format.".to_string())
320 }
321 }
322 Self::Validation { field, .. } => {
323 match field.as_str() {
324 "project_name" => Some("Project names should be lowercase with hyphens or underscores, e.g., 'my-project' or 'my_project'.".to_string()),
325 "spec_name" => Some("Specification names should be in snake_case format, e.g., 'user_authentication' or 'api_design'.".to_string()),
326 "task_id" => Some("Task IDs should be unique strings. Use the generated UUID or a meaningful identifier.".to_string()),
327 _ => Some("Check the documentation for the expected format and valid values for this field.".to_string())
328 }
329 }
330 Self::NotFound { resource_type, identifier, .. } => {
331 match resource_type.as_str() {
332 "Project" => Some(format!("Create the project '{}' first using the setup_project tool.", identifier)),
333 "Specification" => Some(format!("Create the specification '{}' first using the create_spec tool, or check if the ID is correct.", identifier)),
334 "Task" => Some("Check that the task ID is correct or create the task first.".to_string()),
335 _ => Some("Verify that the resource exists and the identifier is correct.".to_string())
336 }
337 }
338 Self::AlreadyExists { resource_type, identifier, .. } => {
339 match resource_type.as_str() {
340 "Project" => Some(format!("Choose a different project name or use the existing project '{}'.", identifier)),
341 "Specification" => Some(format!("Choose a different specification name or load the existing specification '{}'.", identifier)),
342 "Task" => Some("Use a different task ID or update the existing task instead.".to_string()),
343 _ => Some("Choose a different identifier or work with the existing resource.".to_string())
344 }
345 }
346 Self::McpProtocol { operation, .. } => {
347 match operation.as_str() {
348 "tool_call" => Some("Check that all required parameters are provided and have correct types.".to_string()),
349 "list_tools" => Some("Ensure the MCP server is properly initialized and running.".to_string()),
350 _ => Some("Check the MCP client configuration and network connectivity.".to_string())
351 }
352 }
353 Self::Configuration { setting, .. } => {
354 match setting.as_str() {
355 "home_directory" => Some("Set the HOME environment variable or run from a user directory.".to_string()),
356 "base_directory" => Some("Ensure the base directory path is valid and accessible.".to_string()),
357 _ => Some("Check the configuration documentation and verify all required settings.".to_string())
358 }
359 }
360 Self::Permission { operation, path, .. } => {
361 if operation.contains("write") || operation.contains("create") {
362 Some(format!("Ensure you have write permissions for '{}' or change the target directory.", path.display()))
363 } else if operation.contains("read") {
364 Some(format!("Ensure you have read permissions for '{}'.", path.display()))
365 } else {
366 Some("Check that you have the necessary permissions for this operation.".to_string())
367 }
368 }
369 Self::Internal { .. } => {
370 Some("This is an unexpected error. Please report this issue with the error details.".to_string())
371 }
372 }
373 }
374
375 pub fn get_troubleshooting_steps(&self) -> Vec<String> {
377 let mut steps = Vec::new();
378
379 match self {
380 Self::FileSystem { operation, path, source } => {
381 steps.push(format!("1. Check if path '{}' exists", path.display()));
382 if operation.contains("write") || operation.contains("create") {
383 steps.push("2. Verify you have write permissions to the directory".to_string());
384 steps.push("3. Check available disk space".to_string());
385 steps.push("4. Ensure parent directories exist".to_string());
386 } else if operation.contains("read") {
387 steps.push("2. Verify you have read permissions to the file".to_string());
388 steps.push("3. Check that the file is not locked by another process".to_string());
389 }
390 steps.push(format!("5. System error: {}", source));
391 }
392 Self::NotFound { resource_type, identifier, context } => {
393 match resource_type.as_str() {
394 "Project" => {
395 steps.push("1. List available projects to verify the name".to_string());
396 steps.push(format!("2. Create project '{}' using setup_project tool", identifier));
397 steps.push("3. Check for typos in the project name".to_string());
398 }
399 "Specification" => {
400 steps.push("1. List available specifications for the project".to_string());
401 steps.push(format!("2. Create specification '{}' using create_spec tool", identifier));
402 steps.push("3. Verify the specification ID format (YYYYMMDD_name)".to_string());
403 }
404 _ => {
405 steps.push("1. Verify the resource identifier is correct".to_string());
406 steps.push("2. Check if the resource exists in the expected location".to_string());
407 if let Some(ctx) = context {
408 steps.push(format!("3. Context: {}", ctx));
409 }
410 }
411 }
412 }
413 Self::Validation { field, value, reason } => {
414 steps.push(format!("1. Current value '{}' is invalid", value));
415 steps.push(format!("2. Reason: {}", reason));
416 if let Some(suggestion) = self.get_suggestion() {
417 steps.push(format!("3. {}", suggestion));
418 }
419 steps.push(format!("4. Check the documentation for field '{}'", field));
420 }
421 _ => {
422 if let Some(suggestion) = self.get_suggestion() {
423 steps.push(format!("1. {}", suggestion));
424 }
425 steps.push("2. Check the logs for more detailed error information".to_string());
426 steps.push("3. Verify your environment and configuration".to_string());
427 }
428 }
429
430 steps
431 }
432
433 pub fn debug_message(&self) -> String {
435 match self {
436 Self::FileSystem {
437 operation,
438 path,
439 source,
440 } => {
441 format!("FileSystem error: {} at {:?} - {}", operation, path, source)
442 }
443 Self::Serialization {
444 operation,
445 content,
446 source,
447 } => {
448 format!(
449 "Serialization error: {} - content: {} - {}",
450 operation, content, source
451 )
452 }
453 Self::Validation {
454 field,
455 value,
456 reason,
457 } => {
458 format!(
459 "Validation error: field '{}' with value '{}' - {}",
460 field, value, reason
461 )
462 }
463 Self::NotFound {
464 resource_type,
465 identifier,
466 context,
467 } => {
468 format!(
469 "NotFound error: {} '{}' in context '{:?}'",
470 resource_type, identifier, context
471 )
472 }
473 Self::AlreadyExists {
474 resource_type,
475 identifier,
476 context,
477 } => {
478 format!(
479 "AlreadyExists error: {} '{}' in context '{:?}'",
480 resource_type, identifier, context
481 )
482 }
483 Self::McpProtocol {
484 operation,
485 details,
486 source,
487 } => {
488 format!(
489 "MCP Protocol error: {} - {} - source: {:?}",
490 operation, details, source
491 )
492 }
493 Self::Configuration {
494 setting,
495 value,
496 reason,
497 } => {
498 format!(
499 "Configuration error: setting '{}' with value '{}' - {}",
500 setting, value, reason
501 )
502 }
503 Self::Permission {
504 operation,
505 path,
506 details,
507 } => {
508 format!(
509 "Permission error: {} at {:?} - {}",
510 operation, path, details
511 )
512 }
513 Self::Internal {
514 operation,
515 details,
516 source,
517 } => {
518 format!(
519 "Internal error: {} - {} - source: {:?}",
520 operation, details, source
521 )
522 }
523 }
524 }
525
526 pub fn category(&self) -> &'static str {
528 match self {
529 Self::FileSystem { .. } => "filesystem",
530 Self::Serialization { .. } => "serialization",
531 Self::Validation { .. } => "validation",
532 Self::NotFound { .. } => "not_found",
533 Self::AlreadyExists { .. } => "already_exists",
534 Self::McpProtocol { .. } => "mcp_protocol",
535 Self::Configuration { .. } => "configuration",
536 Self::Permission { .. } => "permission",
537 Self::Internal { .. } => "internal",
538 }
539 }
540
541 pub fn is_user_facing(&self) -> bool {
543 !matches!(self, Self::Internal { .. })
544 }
545}
546
547impl fmt::Display for ProjectManagerError {
548 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549 write!(f, "{}", self.user_message())
550 }
551}
552
553impl std::error::Error for ProjectManagerError {
554 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
555 match self {
556 Self::FileSystem { source, .. } => Some(source),
557 Self::Serialization { source, .. } => Some(source),
558 Self::McpProtocol { source, .. } => source
559 .as_ref()
560 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)),
561 Self::Internal { source, .. } => source
562 .as_ref()
563 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)),
564 _ => None,
565 }
566 }
567}
568
569impl From<std::io::Error> for ProjectManagerError {
571 fn from(err: std::io::Error) -> Self {
572 Self::FileSystem {
573 operation: "perform I/O operation".to_string(),
574 path: PathBuf::new(),
575 source: err,
576 }
577 }
578}
579
580impl From<serde_json::Error> for ProjectManagerError {
581 fn from(err: serde_json::Error) -> Self {
582 Self::Serialization {
583 operation: "parse JSON".to_string(),
584 content: "unknown".to_string(),
585 source: err,
586 }
587 }
588}
589
590pub type Result<T> = std::result::Result<T, ProjectManagerError>;
592
593pub mod helpers {
595 use super::*;
596
597 pub fn invalid_project_name(name: &str) -> ProjectManagerError {
599 ProjectManagerError::validation_error(
600 "project_name",
601 name,
602 "Project names cannot contain special characters or spaces",
603 )
604 }
605
606 pub fn invalid_spec_name(name: &str) -> ProjectManagerError {
608 ProjectManagerError::validation_error(
609 "spec_name",
610 name,
611 "Spec names must be in snake_case format (lowercase with underscores)",
612 )
613 }
614
615 pub fn validation_error(field: &str, value: &str, reason: &str) -> ProjectManagerError {
617 ProjectManagerError::validation_error(field, value, reason)
618 }
619
620 pub fn project_not_found(name: &str) -> ProjectManagerError {
622 ProjectManagerError::not_found("Project", name, None)
623 }
624
625 pub fn spec_not_found(spec_id: &str, project_name: &str) -> ProjectManagerError {
627 ProjectManagerError::not_found("Specification", spec_id, Some(project_name))
628 }
629
630 pub fn project_already_exists(name: &str) -> ProjectManagerError {
632 ProjectManagerError::already_exists("Project", name, None)
633 }
634
635 pub fn spec_already_exists(spec_id: &str, project_name: &str) -> ProjectManagerError {
637 ProjectManagerError::already_exists("Specification", spec_id, Some(project_name))
638 }
639
640 pub fn file_system_error(
642 operation: &str,
643 path: &Path,
644 source: std::io::Error,
645 ) -> ProjectManagerError {
646 ProjectManagerError::file_system_error(operation, path, source)
647 }
648
649 pub fn serialization_error(
651 operation: &str,
652 content: &str,
653 source: serde_json::Error,
654 ) -> ProjectManagerError {
655 ProjectManagerError::serialization_error(operation, content, source)
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662 use std::path::PathBuf;
663
664 #[test]
665 fn test_file_system_error_creation() {
666 let path = PathBuf::from("/test/path");
667 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
668 let error = ProjectManagerError::file_system_error("read file", &path, io_error);
669
670 match error {
671 ProjectManagerError::FileSystem { operation, path: error_path, .. } => {
672 assert_eq!(operation, "read file");
673 assert_eq!(error_path, path);
674 }
675 _ => panic!("Expected FileSystem error"),
676 }
677 }
678
679 #[test]
680 fn test_serialization_error_creation() {
681 let json_result: std::result::Result<serde_json::Value, serde_json::Error> = serde_json::from_str("invalid json");
683 let json_error = json_result.unwrap_err();
684 let error = ProjectManagerError::serialization_error("parse JSON", "invalid json", json_error);
685
686 match error {
687 ProjectManagerError::Serialization { operation, content, .. } => {
688 assert_eq!(operation, "parse JSON");
689 assert_eq!(content, "invalid json");
690 }
691 _ => panic!("Expected Serialization error"),
692 }
693 }
694
695 #[test]
696 fn test_validation_error_creation() {
697 let error = ProjectManagerError::validation_error("project_name", "invalid name", "contains spaces");
698
699 match error {
700 ProjectManagerError::Validation { field, value, reason } => {
701 assert_eq!(field, "project_name");
702 assert_eq!(value, "invalid name");
703 assert_eq!(reason, "contains spaces");
704 }
705 _ => panic!("Expected Validation error"),
706 }
707 }
708
709 #[test]
710 fn test_not_found_error_creation() {
711 let error = ProjectManagerError::not_found("Project", "test-project", Some("workspace"));
712
713 match error {
714 ProjectManagerError::NotFound { resource_type, identifier, context } => {
715 assert_eq!(resource_type, "Project");
716 assert_eq!(identifier, "test-project");
717 assert_eq!(context, Some("workspace".to_string()));
718 }
719 _ => panic!("Expected NotFound error"),
720 }
721 }
722
723 #[test]
724 fn test_already_exists_error_creation() {
725 let error = ProjectManagerError::already_exists("Project", "duplicate-project", Some("workspace"));
726
727 match error {
728 ProjectManagerError::AlreadyExists { resource_type, identifier, context } => {
729 assert_eq!(resource_type, "Project");
730 assert_eq!(identifier, "duplicate-project");
731 assert_eq!(context, Some("workspace".to_string()));
732 }
733 _ => panic!("Expected AlreadyExists error"),
734 }
735 }
736
737 #[test]
738 fn test_user_message_formatting() {
739 let path = PathBuf::from("/test/file.txt");
740 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
741 let error = ProjectManagerError::file_system_error("read file", &path, io_error);
742
743 let message = error.user_message();
744 assert!(message.contains("Failed to read file"));
745 assert!(message.contains("/test/file.txt"));
746 }
747
748 #[test]
749 fn test_user_message_validation_error() {
750 let error = ProjectManagerError::validation_error("project_name", "bad name", "contains invalid characters");
751 let message = error.user_message();
752
753 assert!(message.contains("Invalid value 'bad name'"));
754 assert!(message.contains("for field 'project_name'"));
755 assert!(message.contains("contains invalid characters"));
756 }
757
758 #[test]
759 fn test_error_category() {
760 let fs_error = ProjectManagerError::file_system_error("test", &PathBuf::new(),
761 std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
762 assert_eq!(fs_error.category(), "filesystem");
763
764 let validation_error = ProjectManagerError::validation_error("field", "value", "reason");
765 assert_eq!(validation_error.category(), "validation");
766 }
767
768 #[test]
769 fn test_is_user_facing() {
770 let validation_error = ProjectManagerError::validation_error("field", "value", "reason");
771 assert!(validation_error.is_user_facing());
772
773 let internal_error = ProjectManagerError::internal_error("op", "details", None);
774 assert!(!internal_error.is_user_facing());
775 }
776
777 #[test]
778 fn test_from_serde_json_error() {
779 let json_result: std::result::Result<serde_json::Value, serde_json::Error> = serde_json::from_str("invalid json");
780 let json_error = json_result.unwrap_err();
781 let pm_error: ProjectManagerError = json_error.into();
782
783 match pm_error {
784 ProjectManagerError::Serialization { operation, content, .. } => {
785 assert_eq!(operation, "parse JSON");
786 assert_eq!(content, "unknown");
787 }
788 _ => panic!("Expected Serialization error from serde_json::Error conversion"),
789 }
790 }
791
792 #[test]
793 fn test_helpers_invalid_project_name() {
794 let error = helpers::invalid_project_name("bad project name");
795
796 match error {
797 ProjectManagerError::Validation { field, value, reason } => {
798 assert_eq!(field, "project_name");
799 assert_eq!(value, "bad project name");
800 assert!(reason.contains("special characters"));
801 }
802 _ => panic!("Expected Validation error"),
803 }
804 }
805
806 #[test]
807 fn test_helpers_serialization_error() {
808 let json_result: std::result::Result<serde_json::Value, serde_json::Error> = serde_json::from_str("invalid json");
809 let json_error = json_result.unwrap_err();
810 let error = helpers::serialization_error("deserialize", "bad json", json_error);
811
812 match error {
813 ProjectManagerError::Serialization { operation, content, .. } => {
814 assert_eq!(operation, "deserialize");
815 assert_eq!(content, "bad json");
816 }
817 _ => panic!("Expected Serialization error"),
818 }
819 }
820
821 #[test]
822 fn test_helpers_project_already_exists() {
823 let error = helpers::project_already_exists("duplicate-project");
824
825 match error {
826 ProjectManagerError::AlreadyExists { resource_type, identifier, context } => {
827 assert_eq!(resource_type, "Project");
828 assert_eq!(identifier, "duplicate-project");
829 assert_eq!(context, None);
830 }
831 _ => panic!("Expected AlreadyExists error"),
832 }
833 }
834}