mockforge_ui/handlers/
workspaces.rs

1//! Workspace management API handlers
2//!
3//! This module provides REST API endpoints for managing multi-tenant workspaces.
4
5use 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/// Workspace management state
18#[derive(Debug, Clone)]
19pub struct WorkspaceState {
20    /// Multi-tenant workspace registry
21    pub registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>,
22}
23
24impl WorkspaceState {
25    /// Create a new workspace state
26    pub fn new(registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>) -> Self {
27        Self { registry }
28    }
29}
30
31/// API response wrapper
32#[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/// Workspace list item for API responses
60#[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/// Workspace creation request
72#[derive(Debug, Clone, Deserialize)]
73pub struct CreateWorkspaceRequest {
74    pub id: String,
75    pub name: String,
76    pub description: Option<String>,
77}
78
79/// Workspace update request
80#[derive(Debug, Clone, Deserialize)]
81pub struct UpdateWorkspaceRequest {
82    pub name: Option<String>,
83    pub description: Option<String>,
84    pub enabled: Option<bool>,
85}
86
87/// List all workspaces
88pub 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
118/// Get a specific workspace by ID
119pub 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
150/// Create a new workspace
151pub 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    // Check if workspace already exists
158    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    // Create new workspace
167    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            // Get the created workspace
173            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
206/// Update an existing workspace
207pub 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    // Get existing workspace
215    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    // Update workspace fields
227    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    // Save updated workspace
238    match registry.update_workspace(&workspace_id, tenant_ws.workspace.clone()) {
239        Ok(_) => {
240            // Handle enabled/disabled separately
241            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            // Get updated workspace
248            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
281/// Delete a workspace
282pub 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
303/// Get workspace statistics
304pub 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/// Mock environment response
324#[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/// Mock environment manager response
335#[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
342/// List all mock environments for a workspace
343pub 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
393/// Get a specific mock environment
394pub 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/// Set active mock environment
446#[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                    // Save the updated workspace
476                    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/// Update mock environment configuration
513#[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            // Parse the configs from JSON
543            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            // Update the environment config
550            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                    // Save the updated workspace
558                    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                    // Get the updated environment
570                    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    // ==================== WorkspaceState Tests ====================
627
628    #[test]
629    fn test_workspace_state_creation() {
630        let state = create_test_state();
631        // State is created - this verifies the type is correct
632        let _ = state;
633    }
634
635    #[test]
636    fn test_workspace_state_clone() {
637        let state = create_test_state();
638        let cloned = state.clone();
639        // Both states reference the same registry
640        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    // ==================== ApiResponse Tests ====================
651
652    #[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    // ==================== CreateWorkspaceRequest Tests ====================
685
686    #[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    // ==================== UpdateWorkspaceRequest Tests ====================
724
725    #[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    // ==================== WorkspaceListItem Tests ====================
763
764    #[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    // ==================== MockEnvironmentResponse Tests ====================
815
816    #[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    // ==================== MockEnvironmentManagerResponse Tests ====================
864
865    #[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    // ==================== SetActiveEnvironmentRequest Tests ====================
907
908    #[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    // ==================== UpdateMockEnvironmentRequest Tests ====================
925
926    #[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    // ==================== Handler Tests ====================
950
951    #[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        // Create a workspace first
972        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        // Create a workspace first
991        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        // Create a workspace first
1041        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        // Verify workspace is gone
1055        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        // Create a workspace first
1064        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        // Update the workspace
1073        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        // Create a workspace first
1097        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}