1use axum::{
6 extract::{Path, State},
7 http::StatusCode,
8 response::{IntoResponse, Json, Response},
9};
10use mockforge_core::{
11 workspace::MockEnvironmentName, MultiTenantWorkspaceRegistry, Workspace, WorkspaceStats,
12};
13use serde::{Deserialize, Serialize};
14use serde_json::json;
15use std::sync::Arc;
16
17#[derive(Debug, Clone)]
19pub struct WorkspaceState {
20 pub registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>,
22}
23
24impl WorkspaceState {
25 pub fn new(registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>) -> Self {
27 Self { registry }
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ApiResponse<T> {
34 pub success: bool,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub data: Option<T>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub error: Option<String>,
39}
40
41impl<T: Serialize> ApiResponse<T> {
42 pub fn success(data: T) -> Self {
43 Self {
44 success: true,
45 data: Some(data),
46 error: None,
47 }
48 }
49
50 pub fn error(message: String) -> Self {
51 Self {
52 success: false,
53 data: None,
54 error: Some(message),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct WorkspaceListItem {
62 pub id: String,
63 pub name: String,
64 pub description: Option<String>,
65 pub enabled: bool,
66 pub stats: WorkspaceStats,
67 pub created_at: String,
68 pub updated_at: String,
69}
70
71#[derive(Debug, Clone, Deserialize)]
73pub struct CreateWorkspaceRequest {
74 pub id: String,
75 pub name: String,
76 pub description: Option<String>,
77}
78
79#[derive(Debug, Clone, Deserialize)]
81pub struct UpdateWorkspaceRequest {
82 pub name: Option<String>,
83 pub description: Option<String>,
84 pub enabled: Option<bool>,
85}
86
87pub async fn list_workspaces(
89 State(state): State<WorkspaceState>,
90) -> Result<Json<ApiResponse<Vec<WorkspaceListItem>>>, Response> {
91 let registry = state.registry.read().await;
92
93 match registry.list_workspaces() {
94 Ok(workspaces) => {
95 let items: Vec<WorkspaceListItem> = workspaces
96 .into_iter()
97 .map(|(id, tenant_ws)| WorkspaceListItem {
98 id,
99 name: tenant_ws.workspace.name.clone(),
100 description: tenant_ws.workspace.description.clone(),
101 enabled: tenant_ws.enabled,
102 stats: tenant_ws.stats.clone(),
103 created_at: tenant_ws.workspace.created_at.to_rfc3339(),
104 updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
105 })
106 .collect();
107
108 Ok(Json(ApiResponse::success(items)))
109 }
110 Err(e) => {
111 tracing::error!("Failed to list workspaces: {}", e);
112 Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
113 .into_response())
114 }
115 }
116}
117
118pub async fn get_workspace(
120 State(state): State<WorkspaceState>,
121 Path(workspace_id): Path<String>,
122) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
123 let registry = state.registry.read().await;
124
125 match registry.get_workspace(&workspace_id) {
126 Ok(tenant_ws) => {
127 let item = WorkspaceListItem {
128 id: workspace_id.clone(),
129 name: tenant_ws.workspace.name.clone(),
130 description: tenant_ws.workspace.description.clone(),
131 enabled: tenant_ws.enabled,
132 stats: tenant_ws.stats.clone(),
133 created_at: tenant_ws.workspace.created_at.to_rfc3339(),
134 updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
135 };
136
137 Ok(Json(ApiResponse::success(item)))
138 }
139 Err(e) => {
140 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
141 Err((
142 StatusCode::NOT_FOUND,
143 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
144 )
145 .into_response())
146 }
147 }
148}
149
150pub async fn create_workspace(
152 State(state): State<WorkspaceState>,
153 Json(request): Json<CreateWorkspaceRequest>,
154) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
155 let mut registry = state.registry.write().await;
156
157 if registry.workspace_exists(&request.id) {
159 return Err((
160 StatusCode::CONFLICT,
161 Json(json!({"error": format!("Workspace '{}' already exists", request.id)})),
162 )
163 .into_response());
164 }
165
166 let mut workspace = Workspace::new(request.name.clone());
168 workspace.description = request.description.clone();
169
170 match registry.register_workspace(request.id.clone(), workspace) {
171 Ok(_) => {
172 match registry.get_workspace(&request.id) {
174 Ok(tenant_ws) => {
175 let item = WorkspaceListItem {
176 id: request.id.clone(),
177 name: tenant_ws.workspace.name.clone(),
178 description: tenant_ws.workspace.description.clone(),
179 enabled: tenant_ws.enabled,
180 stats: tenant_ws.stats.clone(),
181 created_at: tenant_ws.workspace.created_at.to_rfc3339(),
182 updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
183 };
184
185 tracing::info!("Created workspace: {}", request.id);
186 Ok(Json(ApiResponse::success(item)))
187 }
188 Err(e) => {
189 tracing::error!("Failed to retrieve created workspace: {}", e);
190 Err((
191 StatusCode::INTERNAL_SERVER_ERROR,
192 Json(json!({"error": "Workspace created but failed to retrieve"})),
193 )
194 .into_response())
195 }
196 }
197 }
198 Err(e) => {
199 tracing::error!("Failed to create workspace: {}", e);
200 Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
201 .into_response())
202 }
203 }
204}
205
206pub async fn update_workspace(
208 State(state): State<WorkspaceState>,
209 Path(workspace_id): Path<String>,
210 Json(request): Json<UpdateWorkspaceRequest>,
211) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
212 let mut registry = state.registry.write().await;
213
214 let mut tenant_ws = match registry.get_workspace(&workspace_id) {
216 Ok(ws) => ws,
217 Err(e) => {
218 return Err((
219 StatusCode::NOT_FOUND,
220 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
221 )
222 .into_response());
223 }
224 };
225
226 if let Some(name) = request.name {
228 tenant_ws.workspace.name = name;
229 }
230
231 if let Some(description) = request.description {
232 tenant_ws.workspace.description = Some(description);
233 }
234
235 tenant_ws.workspace.updated_at = chrono::Utc::now();
236
237 match registry.update_workspace(&workspace_id, tenant_ws.workspace.clone()) {
239 Ok(_) => {
240 if let Some(enabled) = request.enabled {
242 if let Err(e) = registry.set_workspace_enabled(&workspace_id, enabled) {
243 tracing::error!("Failed to set workspace enabled status: {}", e);
244 }
245 }
246
247 match registry.get_workspace(&workspace_id) {
249 Ok(updated_ws) => {
250 let item = WorkspaceListItem {
251 id: workspace_id.clone(),
252 name: updated_ws.workspace.name.clone(),
253 description: updated_ws.workspace.description.clone(),
254 enabled: updated_ws.enabled,
255 stats: updated_ws.stats.clone(),
256 created_at: updated_ws.workspace.created_at.to_rfc3339(),
257 updated_at: updated_ws.workspace.updated_at.to_rfc3339(),
258 };
259
260 tracing::info!("Updated workspace: {}", workspace_id);
261 Ok(Json(ApiResponse::success(item)))
262 }
263 Err(e) => {
264 tracing::error!("Failed to retrieve updated workspace: {}", e);
265 Err((
266 StatusCode::INTERNAL_SERVER_ERROR,
267 Json(json!({"error": "Workspace updated but failed to retrieve"})),
268 )
269 .into_response())
270 }
271 }
272 }
273 Err(e) => {
274 tracing::error!("Failed to update workspace: {}", e);
275 Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
276 .into_response())
277 }
278 }
279}
280
281pub async fn delete_workspace(
283 State(state): State<WorkspaceState>,
284 Path(workspace_id): Path<String>,
285) -> Result<Json<ApiResponse<String>>, Response> {
286 let mut registry = state.registry.write().await;
287
288 match registry.remove_workspace(&workspace_id) {
289 Ok(_) => {
290 tracing::info!("Deleted workspace: {}", workspace_id);
291 Ok(Json(ApiResponse::success(format!(
292 "Workspace '{}' deleted successfully",
293 workspace_id
294 ))))
295 }
296 Err(e) => {
297 tracing::error!("Failed to delete workspace {}: {}", workspace_id, e);
298 Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response())
299 }
300 }
301}
302
303pub async fn get_workspace_stats(
305 State(state): State<WorkspaceState>,
306 Path(workspace_id): Path<String>,
307) -> Result<Json<ApiResponse<WorkspaceStats>>, Response> {
308 let registry = state.registry.read().await;
309
310 match registry.get_workspace(&workspace_id) {
311 Ok(tenant_ws) => Ok(Json(ApiResponse::success(tenant_ws.stats.clone()))),
312 Err(e) => {
313 tracing::error!("Failed to get workspace stats for {}: {}", workspace_id, e);
314 Err((
315 StatusCode::NOT_FOUND,
316 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
317 )
318 .into_response())
319 }
320 }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct MockEnvironmentResponse {
326 pub name: String,
327 pub id: String,
328 pub workspace_id: String,
329 pub reality_config: Option<serde_json::Value>,
330 pub chaos_config: Option<serde_json::Value>,
331 pub drift_budget_config: Option<serde_json::Value>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct MockEnvironmentManagerResponse {
337 pub workspace_id: String,
338 pub active_environment: Option<String>,
339 pub environments: Vec<MockEnvironmentResponse>,
340}
341
342pub async fn list_mock_environments(
344 State(state): State<WorkspaceState>,
345 Path(workspace_id): Path<String>,
346) -> Result<Json<ApiResponse<MockEnvironmentManagerResponse>>, Response> {
347 let registry = state.registry.read().await;
348
349 match registry.get_workspace(&workspace_id) {
350 Ok(tenant_ws) => {
351 let mock_envs = tenant_ws.workspace.get_mock_environments();
352 let environments: Vec<MockEnvironmentResponse> = mock_envs
353 .list_environments()
354 .into_iter()
355 .map(|env| MockEnvironmentResponse {
356 name: env.name.as_str().to_string(),
357 id: env.id.clone(),
358 workspace_id: env.workspace_id.clone(),
359 reality_config: env
360 .reality_config
361 .as_ref()
362 .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
363 chaos_config: env
364 .chaos_config
365 .as_ref()
366 .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
367 drift_budget_config: env
368 .drift_budget_config
369 .as_ref()
370 .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
371 })
372 .collect();
373
374 let response = MockEnvironmentManagerResponse {
375 workspace_id: workspace_id.clone(),
376 active_environment: mock_envs.active_environment.map(|e| e.as_str().to_string()),
377 environments,
378 };
379
380 Ok(Json(ApiResponse::success(response)))
381 }
382 Err(e) => {
383 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
384 Err((
385 StatusCode::NOT_FOUND,
386 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
387 )
388 .into_response())
389 }
390 }
391}
392
393pub async fn get_mock_environment(
395 State(state): State<WorkspaceState>,
396 Path((workspace_id, env_name)): Path<(String, String)>,
397) -> Result<Json<ApiResponse<MockEnvironmentResponse>>, Response> {
398 let registry = state.registry.read().await;
399
400 let env_name_enum = match env_name.to_lowercase().as_str() {
401 "dev" => MockEnvironmentName::Dev,
402 "test" => MockEnvironmentName::Test,
403 "prod" => MockEnvironmentName::Prod,
404 _ => {
405 return Err((
406 StatusCode::BAD_REQUEST,
407 Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", env_name)})),
408 )
409 .into_response());
410 }
411 };
412
413 match registry.get_workspace(&workspace_id) {
414 Ok(tenant_ws) => {
415 match tenant_ws.workspace.get_mock_environment(env_name_enum) {
416 Some(env) => {
417 let response = MockEnvironmentResponse {
418 name: env.name.as_str().to_string(),
419 id: env.id.clone(),
420 workspace_id: env.workspace_id.clone(),
421 reality_config: env.reality_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
422 chaos_config: env.chaos_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
423 drift_budget_config: env.drift_budget_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
424 };
425 Ok(Json(ApiResponse::success(response)))
426 }
427 None => Err((
428 StatusCode::NOT_FOUND,
429 Json(json!({"error": format!("Environment '{}' not found in workspace '{}'", env_name, workspace_id)})),
430 )
431 .into_response()),
432 }
433 }
434 Err(e) => {
435 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
436 Err((
437 StatusCode::NOT_FOUND,
438 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
439 )
440 .into_response())
441 }
442 }
443}
444
445#[derive(Debug, Clone, Deserialize)]
447pub struct SetActiveEnvironmentRequest {
448 pub environment: String,
449}
450
451pub async fn set_active_mock_environment(
452 State(state): State<WorkspaceState>,
453 Path(workspace_id): Path<String>,
454 Json(request): Json<SetActiveEnvironmentRequest>,
455) -> Result<Json<ApiResponse<String>>, Response> {
456 let mut registry = state.registry.write().await;
457
458 let env_name = match request.environment.to_lowercase().as_str() {
459 "dev" => MockEnvironmentName::Dev,
460 "test" => MockEnvironmentName::Test,
461 "prod" => MockEnvironmentName::Prod,
462 _ => {
463 return Err((
464 StatusCode::BAD_REQUEST,
465 Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", request.environment)})),
466 )
467 .into_response());
468 }
469 };
470
471 match registry.get_workspace(&workspace_id) {
472 Ok(mut tenant_ws) => {
473 match tenant_ws.workspace.set_active_mock_environment(env_name) {
474 Ok(_) => {
475 if let Err(e) =
477 registry.update_workspace(&workspace_id, tenant_ws.workspace.clone())
478 {
479 tracing::error!("Failed to save workspace: {}", e);
480 return Err((
481 StatusCode::INTERNAL_SERVER_ERROR,
482 Json(json!({"error": "Failed to save workspace"})),
483 )
484 .into_response());
485 }
486
487 tracing::info!(
488 "Set active environment to '{}' for workspace '{}'",
489 request.environment,
490 workspace_id
491 );
492 Ok(Json(ApiResponse::success(format!(
493 "Active environment set to '{}'",
494 request.environment
495 ))))
496 }
497 Err(e) => Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()})))
498 .into_response()),
499 }
500 }
501 Err(e) => {
502 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
503 Err((
504 StatusCode::NOT_FOUND,
505 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
506 )
507 .into_response())
508 }
509 }
510}
511
512#[derive(Debug, Clone, Deserialize)]
514pub struct UpdateMockEnvironmentRequest {
515 pub reality_config: Option<serde_json::Value>,
516 pub chaos_config: Option<serde_json::Value>,
517 pub drift_budget_config: Option<serde_json::Value>,
518}
519
520pub async fn update_mock_environment(
521 State(state): State<WorkspaceState>,
522 Path((workspace_id, env_name)): Path<(String, String)>,
523 Json(request): Json<UpdateMockEnvironmentRequest>,
524) -> Result<Json<ApiResponse<MockEnvironmentResponse>>, Response> {
525 let mut registry = state.registry.write().await;
526
527 let env_name_enum = match env_name.to_lowercase().as_str() {
528 "dev" => MockEnvironmentName::Dev,
529 "test" => MockEnvironmentName::Test,
530 "prod" => MockEnvironmentName::Prod,
531 _ => {
532 return Err((
533 StatusCode::BAD_REQUEST,
534 Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", env_name)})),
535 )
536 .into_response());
537 }
538 };
539
540 match registry.get_workspace(&workspace_id) {
541 Ok(mut tenant_ws) => {
542 let reality_config =
544 request.reality_config.and_then(|v| serde_json::from_value(v).ok());
545 let chaos_config = request.chaos_config.and_then(|v| serde_json::from_value(v).ok());
546 let drift_budget_config =
547 request.drift_budget_config.and_then(|v| serde_json::from_value(v).ok());
548
549 match tenant_ws.workspace.set_mock_environment_config(
551 env_name_enum,
552 reality_config,
553 chaos_config,
554 drift_budget_config,
555 ) {
556 Ok(_) => {
557 if let Err(e) =
559 registry.update_workspace(&workspace_id, tenant_ws.workspace.clone())
560 {
561 tracing::error!("Failed to save workspace: {}", e);
562 return Err((
563 StatusCode::INTERNAL_SERVER_ERROR,
564 Json(json!({"error": "Failed to save workspace"})),
565 )
566 .into_response());
567 }
568
569 match tenant_ws.workspace.get_mock_environment(env_name_enum) {
571 Some(env) => {
572 let response = MockEnvironmentResponse {
573 name: env.name.as_str().to_string(),
574 id: env.id.clone(),
575 workspace_id: env.workspace_id.clone(),
576 reality_config: env.reality_config.as_ref().map(|c| {
577 serde_json::to_value(c).unwrap_or(serde_json::json!({}))
578 }),
579 chaos_config: env.chaos_config.as_ref().map(|c| {
580 serde_json::to_value(c).unwrap_or(serde_json::json!({}))
581 }),
582 drift_budget_config: env.drift_budget_config.as_ref().map(|c| {
583 serde_json::to_value(c).unwrap_or(serde_json::json!({}))
584 }),
585 };
586 tracing::info!(
587 "Updated environment '{}' for workspace '{}'",
588 env_name,
589 workspace_id
590 );
591 Ok(Json(ApiResponse::success(response)))
592 }
593 None => Err((
594 StatusCode::INTERNAL_SERVER_ERROR,
595 Json(json!({"error": "Failed to retrieve updated environment"})),
596 )
597 .into_response()),
598 }
599 }
600 Err(e) => Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()})))
601 .into_response()),
602 }
603 }
604 Err(e) => {
605 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
606 Err((
607 StatusCode::NOT_FOUND,
608 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
609 )
610 .into_response())
611 }
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use mockforge_core::MultiTenantConfig;
619
620 fn create_test_state() -> WorkspaceState {
621 let config = MultiTenantConfig::default();
622 let registry = MultiTenantWorkspaceRegistry::new(config);
623 WorkspaceState::new(Arc::new(tokio::sync::RwLock::new(registry)))
624 }
625
626 #[test]
629 fn test_workspace_state_creation() {
630 let state = create_test_state();
631 let _ = state;
633 }
634
635 #[test]
636 fn test_workspace_state_clone() {
637 let state = create_test_state();
638 let cloned = state.clone();
639 let _ = cloned;
641 }
642
643 #[test]
644 fn test_workspace_state_debug() {
645 let state = create_test_state();
646 let debug = format!("{:?}", state);
647 assert!(debug.contains("WorkspaceState"));
648 }
649
650 #[test]
653 fn test_api_response_success() {
654 let response: ApiResponse<String> = ApiResponse::success("test data".to_string());
655 assert!(response.success);
656 assert!(response.data.is_some());
657 assert!(response.error.is_none());
658 }
659
660 #[test]
661 fn test_api_response_error() {
662 let response: ApiResponse<String> = ApiResponse::error("test error".to_string());
663 assert!(!response.success);
664 assert!(response.data.is_none());
665 assert!(response.error.is_some());
666 }
667
668 #[test]
669 fn test_api_response_serialization() {
670 let response = ApiResponse::success("data".to_string());
671 let json = serde_json::to_string(&response).unwrap();
672 assert!(json.contains("success"));
673 assert!(json.contains("data"));
674 }
675
676 #[test]
677 fn test_api_response_error_serialization() {
678 let response: ApiResponse<()> = ApiResponse::error("something went wrong".to_string());
679 let json = serde_json::to_string(&response).unwrap();
680 assert!(json.contains("error"));
681 assert!(json.contains("something went wrong"));
682 }
683
684 #[test]
687 fn test_create_workspace_request_minimal() {
688 let request = CreateWorkspaceRequest {
689 id: "ws-123".to_string(),
690 name: "My Workspace".to_string(),
691 description: None,
692 };
693
694 assert_eq!(request.id, "ws-123");
695 assert_eq!(request.name, "My Workspace");
696 assert!(request.description.is_none());
697 }
698
699 #[test]
700 fn test_create_workspace_request_full() {
701 let request = CreateWorkspaceRequest {
702 id: "ws-456".to_string(),
703 name: "Full Workspace".to_string(),
704 description: Some("A complete workspace".to_string()),
705 };
706
707 assert!(request.description.is_some());
708 }
709
710 #[test]
711 fn test_create_workspace_request_deserialization() {
712 let json = r#"{
713 "id": "test-ws",
714 "name": "Test",
715 "description": "Test workspace"
716 }"#;
717
718 let request: CreateWorkspaceRequest = serde_json::from_str(json).unwrap();
719 assert_eq!(request.id, "test-ws");
720 assert_eq!(request.name, "Test");
721 }
722
723 #[test]
726 fn test_update_workspace_request_empty() {
727 let request = UpdateWorkspaceRequest {
728 name: None,
729 description: None,
730 enabled: None,
731 };
732
733 assert!(request.name.is_none());
734 assert!(request.description.is_none());
735 assert!(request.enabled.is_none());
736 }
737
738 #[test]
739 fn test_update_workspace_request_partial() {
740 let request = UpdateWorkspaceRequest {
741 name: Some("New Name".to_string()),
742 description: None,
743 enabled: Some(false),
744 };
745
746 assert!(request.name.is_some());
747 assert!(request.enabled.is_some());
748 }
749
750 #[test]
751 fn test_update_workspace_request_deserialization() {
752 let json = r#"{
753 "name": "Updated",
754 "enabled": true
755 }"#;
756
757 let request: UpdateWorkspaceRequest = serde_json::from_str(json).unwrap();
758 assert_eq!(request.name, Some("Updated".to_string()));
759 assert_eq!(request.enabled, Some(true));
760 }
761
762 #[test]
765 fn test_workspace_list_item_creation() {
766 let item = WorkspaceListItem {
767 id: "item-1".to_string(),
768 name: "Test Item".to_string(),
769 description: Some("Description".to_string()),
770 enabled: true,
771 stats: WorkspaceStats::default(),
772 created_at: "2024-01-01T00:00:00Z".to_string(),
773 updated_at: "2024-01-02T00:00:00Z".to_string(),
774 };
775
776 assert_eq!(item.id, "item-1");
777 assert!(item.enabled);
778 }
779
780 #[test]
781 fn test_workspace_list_item_serialization() {
782 let item = WorkspaceListItem {
783 id: "ser-test".to_string(),
784 name: "Serialize Test".to_string(),
785 description: None,
786 enabled: false,
787 stats: WorkspaceStats::default(),
788 created_at: "2024-01-01T00:00:00Z".to_string(),
789 updated_at: "2024-01-01T00:00:00Z".to_string(),
790 };
791
792 let json = serde_json::to_string(&item).unwrap();
793 assert!(json.contains("ser-test"));
794 assert!(json.contains("Serialize Test"));
795 }
796
797 #[test]
798 fn test_workspace_list_item_clone() {
799 let item = WorkspaceListItem {
800 id: "clone-test".to_string(),
801 name: "Clone Test".to_string(),
802 description: None,
803 enabled: true,
804 stats: WorkspaceStats::default(),
805 created_at: "2024-01-01T00:00:00Z".to_string(),
806 updated_at: "2024-01-01T00:00:00Z".to_string(),
807 };
808
809 let cloned = item.clone();
810 assert_eq!(cloned.id, item.id);
811 assert_eq!(cloned.enabled, item.enabled);
812 }
813
814 #[test]
817 fn test_mock_environment_response_creation() {
818 let response = MockEnvironmentResponse {
819 name: "dev".to_string(),
820 id: "env-123".to_string(),
821 workspace_id: "ws-456".to_string(),
822 reality_config: None,
823 chaos_config: None,
824 drift_budget_config: None,
825 };
826
827 assert_eq!(response.name, "dev");
828 assert_eq!(response.id, "env-123");
829 }
830
831 #[test]
832 fn test_mock_environment_response_with_configs() {
833 let response = MockEnvironmentResponse {
834 name: "test".to_string(),
835 id: "env-test".to_string(),
836 workspace_id: "ws-test".to_string(),
837 reality_config: Some(serde_json::json!({"level": "high"})),
838 chaos_config: Some(serde_json::json!({"enabled": true})),
839 drift_budget_config: Some(serde_json::json!({"max_drift": 0.1})),
840 };
841
842 assert!(response.reality_config.is_some());
843 assert!(response.chaos_config.is_some());
844 assert!(response.drift_budget_config.is_some());
845 }
846
847 #[test]
848 fn test_mock_environment_response_serialization() {
849 let response = MockEnvironmentResponse {
850 name: "prod".to_string(),
851 id: "env-prod".to_string(),
852 workspace_id: "ws-prod".to_string(),
853 reality_config: None,
854 chaos_config: None,
855 drift_budget_config: None,
856 };
857
858 let json = serde_json::to_string(&response).unwrap();
859 assert!(json.contains("prod"));
860 assert!(json.contains("env-prod"));
861 }
862
863 #[test]
866 fn test_mock_environment_manager_response_empty() {
867 let response = MockEnvironmentManagerResponse {
868 workspace_id: "ws-empty".to_string(),
869 active_environment: None,
870 environments: vec![],
871 };
872
873 assert!(response.active_environment.is_none());
874 assert!(response.environments.is_empty());
875 }
876
877 #[test]
878 fn test_mock_environment_manager_response_with_environments() {
879 let response = MockEnvironmentManagerResponse {
880 workspace_id: "ws-full".to_string(),
881 active_environment: Some("dev".to_string()),
882 environments: vec![
883 MockEnvironmentResponse {
884 name: "dev".to_string(),
885 id: "env-dev".to_string(),
886 workspace_id: "ws-full".to_string(),
887 reality_config: None,
888 chaos_config: None,
889 drift_budget_config: None,
890 },
891 MockEnvironmentResponse {
892 name: "test".to_string(),
893 id: "env-test".to_string(),
894 workspace_id: "ws-full".to_string(),
895 reality_config: None,
896 chaos_config: None,
897 drift_budget_config: None,
898 },
899 ],
900 };
901
902 assert_eq!(response.active_environment, Some("dev".to_string()));
903 assert_eq!(response.environments.len(), 2);
904 }
905
906 #[test]
909 fn test_set_active_environment_request_creation() {
910 let request = SetActiveEnvironmentRequest {
911 environment: "prod".to_string(),
912 };
913
914 assert_eq!(request.environment, "prod");
915 }
916
917 #[test]
918 fn test_set_active_environment_request_deserialization() {
919 let json = r#"{"environment": "test"}"#;
920 let request: SetActiveEnvironmentRequest = serde_json::from_str(json).unwrap();
921 assert_eq!(request.environment, "test");
922 }
923
924 #[test]
927 fn test_update_mock_environment_request_empty() {
928 let request = UpdateMockEnvironmentRequest {
929 reality_config: None,
930 chaos_config: None,
931 drift_budget_config: None,
932 };
933
934 assert!(request.reality_config.is_none());
935 }
936
937 #[test]
938 fn test_update_mock_environment_request_with_configs() {
939 let request = UpdateMockEnvironmentRequest {
940 reality_config: Some(serde_json::json!({"level": "medium"})),
941 chaos_config: Some(serde_json::json!({"rate": 0.5})),
942 drift_budget_config: None,
943 };
944
945 assert!(request.reality_config.is_some());
946 assert!(request.chaos_config.is_some());
947 }
948
949 #[tokio::test]
952 async fn test_create_workspace() {
953 let state = create_test_state();
954
955 let request = CreateWorkspaceRequest {
956 id: "test".to_string(),
957 name: "Test Workspace".to_string(),
958 description: Some("Test description".to_string()),
959 };
960
961 let result = create_workspace(State(state.clone()), Json(request)).await.unwrap();
962
963 assert!(result.0.success);
964 assert_eq!(result.0.data.as_ref().unwrap().id, "test");
965 }
966
967 #[tokio::test]
968 async fn test_list_workspaces() {
969 let state = create_test_state();
970
971 let request = CreateWorkspaceRequest {
973 id: "test".to_string(),
974 name: "Test Workspace".to_string(),
975 description: None,
976 };
977
978 let _ = create_workspace(State(state.clone()), Json(request)).await;
979
980 let result = list_workspaces(State(state)).await.unwrap();
981
982 assert!(result.0.success);
983 assert!(!result.0.data.unwrap().is_empty());
984 }
985
986 #[tokio::test]
987 async fn test_get_workspace() {
988 let state = create_test_state();
989
990 let request = CreateWorkspaceRequest {
992 id: "get-test".to_string(),
993 name: "Get Test Workspace".to_string(),
994 description: None,
995 };
996
997 let _ = create_workspace(State(state.clone()), Json(request)).await;
998
999 let result = get_workspace(State(state), Path("get-test".to_string())).await.unwrap();
1000
1001 assert!(result.0.success);
1002 assert_eq!(result.0.data.as_ref().unwrap().id, "get-test");
1003 }
1004
1005 #[tokio::test]
1006 async fn test_get_workspace_not_found() {
1007 let state = create_test_state();
1008
1009 let result = get_workspace(State(state), Path("nonexistent".to_string())).await;
1010
1011 assert!(result.is_err());
1012 }
1013
1014 #[tokio::test]
1015 async fn test_create_duplicate_workspace() {
1016 let state = create_test_state();
1017
1018 let request = CreateWorkspaceRequest {
1019 id: "duplicate".to_string(),
1020 name: "First".to_string(),
1021 description: None,
1022 };
1023
1024 let _ = create_workspace(State(state.clone()), Json(request)).await;
1025
1026 let request2 = CreateWorkspaceRequest {
1027 id: "duplicate".to_string(),
1028 name: "Second".to_string(),
1029 description: None,
1030 };
1031
1032 let result = create_workspace(State(state), Json(request2)).await;
1033 assert!(result.is_err());
1034 }
1035
1036 #[tokio::test]
1037 async fn test_delete_workspace() {
1038 let state = create_test_state();
1039
1040 let request = CreateWorkspaceRequest {
1042 id: "delete-test".to_string(),
1043 name: "Delete Test".to_string(),
1044 description: None,
1045 };
1046
1047 let _ = create_workspace(State(state.clone()), Json(request)).await;
1048
1049 let result = delete_workspace(State(state.clone()), Path("delete-test".to_string())).await;
1050
1051 assert!(result.is_ok());
1052 assert!(result.unwrap().0.success);
1053
1054 let get_result = get_workspace(State(state), Path("delete-test".to_string())).await;
1056 assert!(get_result.is_err());
1057 }
1058
1059 #[tokio::test]
1060 async fn test_update_workspace() {
1061 let state = create_test_state();
1062
1063 let create_request = CreateWorkspaceRequest {
1065 id: "update-test".to_string(),
1066 name: "Original Name".to_string(),
1067 description: None,
1068 };
1069
1070 let _ = create_workspace(State(state.clone()), Json(create_request)).await;
1071
1072 let update_request = UpdateWorkspaceRequest {
1074 name: Some("Updated Name".to_string()),
1075 description: Some("New description".to_string()),
1076 enabled: Some(false),
1077 };
1078
1079 let result = update_workspace(
1080 State(state.clone()),
1081 Path("update-test".to_string()),
1082 Json(update_request),
1083 )
1084 .await;
1085
1086 assert!(result.is_ok());
1087 let response = result.unwrap();
1088 assert!(response.0.success);
1089 assert_eq!(response.0.data.as_ref().unwrap().name, "Updated Name");
1090 }
1091
1092 #[tokio::test]
1093 async fn test_get_workspace_stats() {
1094 let state = create_test_state();
1095
1096 let request = CreateWorkspaceRequest {
1098 id: "stats-test".to_string(),
1099 name: "Stats Test".to_string(),
1100 description: None,
1101 };
1102
1103 let _ = create_workspace(State(state.clone()), Json(request)).await;
1104
1105 let result = get_workspace_stats(State(state), Path("stats-test".to_string())).await;
1106
1107 assert!(result.is_ok());
1108 assert!(result.unwrap().0.success);
1109 }
1110}