diff --git a/Cargo.lock b/Cargo.lock index 2f68e41..8f04441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,6 +549,7 @@ dependencies = [ "crossterm", "dirs 6.0.0", "dotenvy", + "futures", "lazy_static", "prost", "prost-types", diff --git a/client/Cargo.toml b/client/Cargo.toml index 71e1c22..6c66592 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -36,3 +36,4 @@ ui-debug = [] rstest = "0.25.0" tokio-test = "0.4.4" uuid = { version = "1.17.0", features = ["v4"] } +futures = "0.3.31" diff --git a/client/tests/form/requests/form_request_tests.rs b/client/tests/form/requests/form_request_tests.rs index 776d699..06c1ace 100644 --- a/client/tests/form/requests/form_request_tests.rs +++ b/client/tests/form/requests/form_request_tests.rs @@ -156,4 +156,863 @@ async fn populated_test_context() -> FormTestContext { context } +#[rstest] +#[tokio::test] +async fn test_post_table_data_success(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let test_data = context.create_test_form_data(); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + test_data.clone(), + ).await; + + match result { + Ok(response) => { + assert!(response.success, "POST operation should succeed"); + assert!(response.inserted_id > 0, "Should return valid inserted ID"); + assert!(!response.message.is_empty(), "Should return non-empty message"); + println!("POST successful: ID {}, Message: {}", response.inserted_id, response.message); + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + if status.code() == tonic::Code::Unavailable { + println!("Backend unavailable - test cannot run"); + return; + } + } + panic!("POST request failed unexpectedly: {}", e); + } + } +} + +#[rstest] +#[tokio::test] +async fn test_post_table_data_minimal_data(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let minimal_data = context.create_minimal_form_data(); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + minimal_data, + ).await; + + assert!(result.is_ok(), "POST with minimal data should succeed"); + let response = result.unwrap(); + assert!(response.success); + assert!(response.inserted_id > 0); +} + +#[rstest] +#[tokio::test] +async fn test_get_table_data_count_success(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let result = context.client.get_table_data_count( + context.profile_name.clone(), + context.table_name.clone(), + ).await; + + match result { + Ok(count) => { + assert!(count >= 0, "Count should be non-negative"); + println!("GET count successful: {}", count); + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + if status.code() == tonic::Code::Unavailable { + println!("Backend unavailable - test cannot run"); + return; + } + } + panic!("GET count request failed unexpectedly: {}", e); + } + } +} + +#[rstest] +#[tokio::test] +async fn test_get_table_data_by_id_with_existing_record(#[future] populated_test_context: FormTestContext) { + let mut context = populated_test_context.await; + skip_if_backend_unavailable!(); + + // First create a record + let test_data = context.create_test_form_data(); + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + test_data.clone(), + ).await; + + if let Ok(post_response) = post_result { + let created_id = post_response.inserted_id; + + // Now try to get it + let get_result = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ).await; + + match get_result { + Ok(response) => { + assert!(!response.data.is_empty(), "Should return data fields"); + println!("GET by ID successful: {} fields", response.data.len()); + + // Verify some data matches + if let Some(firma_value) = response.data.get("firma") { + assert_eq!(firma_value, "Test Company Ltd"); + } + } + Err(e) => panic!("GET by ID failed unexpectedly: {}", e), + } + } else { + println!("Could not create test record, skipping GET test"); + } +} + +#[rstest] +#[tokio::test] +async fn test_get_table_data_by_nonexistent_id(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let nonexistent_id = 99999; + let result = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + nonexistent_id, + ).await; + + assert!(result.is_err(), "GET should fail for nonexistent ID"); + if let Some(status) = result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::NotFound); + } +} + +#[rstest] +#[tokio::test] +async fn test_put_table_data_success(#[future] populated_test_context: FormTestContext) { + let mut context = populated_test_context.await; + skip_if_backend_unavailable!(); + + // First create a record + let test_data = context.create_test_form_data(); + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + test_data, + ).await; + + if let Ok(post_response) = post_result { + let created_id = post_response.inserted_id; + + // Update the record + let mut update_data = HashMap::new(); + update_data.insert("firma".to_string(), create_string_value("Updated Company Name")); + update_data.insert("telefon".to_string(), create_string_value("+421987654321")); + + let put_result = context.client.put_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + update_data, + ).await; + + match put_result { + Ok(response) => { + assert!(response.success, "PUT operation should succeed"); + assert_eq!(response.updated_id, created_id, "Should return correct updated ID"); + println!("PUT successful: ID {}, Message: {}", response.updated_id, response.message); + } + Err(e) => panic!("PUT request failed unexpectedly: {}", e), + } + } else { + println!("Could not create test record, skipping PUT test"); + } +} + +#[rstest] +#[tokio::test] +async fn test_put_table_data_nonexistent_id(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let nonexistent_id = 99999; + let mut update_data = HashMap::new(); + update_data.insert("firma".to_string(), create_string_value("Updated Company")); + + let result = context.client.put_table_data( + context.profile_name.clone(), + context.table_name.clone(), + nonexistent_id, + update_data, + ).await; + + assert!(result.is_err(), "PUT should fail for nonexistent ID"); + if let Some(status) = result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::NotFound); + } +} + +#[rstest] +#[tokio::test] +async fn test_delete_table_data_success(#[future] populated_test_context: FormTestContext) { + let mut context = populated_test_context.await; + skip_if_backend_unavailable!(); + + // First create a record + let test_data = context.create_test_form_data(); + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + test_data, + ).await; + + if let Ok(post_response) = post_result { + let created_id = post_response.inserted_id; + + let delete_result = context.client.delete_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ).await; + + match delete_result { + Ok(response) => { + assert!(response.success, "DELETE operation should succeed"); + println!("DELETE successful for ID {}", created_id); + } + Err(e) => panic!("DELETE request failed unexpectedly: {}", e), + } + } else { + println!("Could not create test record, skipping DELETE test"); + } +} + +#[rstest] +#[tokio::test] +async fn test_delete_table_data_nonexistent_id(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let nonexistent_id = 99999; + let result = context.client.delete_table_data( + context.profile_name.clone(), + context.table_name.clone(), + nonexistent_id, + ).await; + + // DELETE should succeed even for nonexistent IDs (idempotent operation) + assert!(result.is_ok(), "DELETE should not fail for nonexistent ID"); + let response = result.unwrap(); + assert!(response.success, "DELETE should report success even for nonexistent ID"); +} + +// ======================================================================== +// ERROR HANDLING AND VALIDATION TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_invalid_profile_and_table_errors(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let bogus_profile = "profile_does_not_exist".to_string(); + let bogus_table = "table_does_not_exist".to_string(); + + let result = context.client.get_table_data_count(bogus_profile, bogus_table).await; + + assert!(result.is_err(), "Expected error for non-existent profile/table"); + if let Some(status) = result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::NotFound, "Expected NotFound for non-existent profile"); + } +} + +#[rstest] +#[tokio::test] +async fn test_invalid_column_validation(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let invalid_data = context.create_invalid_form_data(); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + invalid_data, + ).await; + + assert!(result.is_err(), "Expected error for undefined column"); + if let Some(status) = result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::InvalidArgument); + assert!(status.message().contains("Invalid column") || + status.message().contains("nonexistent")); + } +} + +#[rstest] +#[tokio::test] +async fn test_data_type_validation(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let type_mismatch_data = context.create_type_mismatch_data(); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + type_mismatch_data, + ).await; + + assert!(result.is_err(), "Expected error for wrong data type"); + if let Some(status) = result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::InvalidArgument); + } +} + +#[rstest] +#[tokio::test] +async fn test_empty_data_validation(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let empty_data = HashMap::new(); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + empty_data, + ).await; + + assert!(result.is_err(), "Expected error for empty data"); + if let Some(status) = result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::InvalidArgument); + } +} + +// ======================================================================== +// SOFT DELETE BEHAVIOR TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_soft_delete_behavior_comprehensive(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Create a record + let test_data = context.create_test_form_data(); + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + test_data, + ).await; + + if let Ok(post_response) = post_result { + let record_id = post_response.inserted_id; + println!("Created record with ID {}", record_id); + + // 2. Verify count before deletion + let count_before = context.client.get_table_data_count( + context.profile_name.clone(), + context.table_name.clone() + ).await.unwrap_or(0); + assert!(count_before >= 1, "Count should be at least 1 after creation"); + + // 3. Soft-delete the record + let delete_result = context.client.delete_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + ).await; + + assert!(delete_result.is_ok(), "Delete operation should succeed"); + println!("Soft-deleted record {}", record_id); + + // 4. Verify count decreased after deletion + let count_after = context.client.get_table_data_count( + context.profile_name.clone(), + context.table_name.clone() + ).await.unwrap_or(0); + assert_eq!(count_after, count_before - 1, "Count should decrease by 1 after soft delete"); + + // 5. Try to GET the soft-deleted record + let get_result = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + ).await; + + assert!(get_result.is_err(), "Should not be able to GET a soft-deleted record"); + if let Some(status) = get_result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::NotFound); + } + println!("Correctly failed to GET soft-deleted record"); + } else { + println!("Could not create test record for soft delete test"); + } +} + +// ======================================================================== +// POSITIONAL RETRIEVAL TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_positional_retrieval_comprehensive(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Create multiple records + let test_names = ["Alice Corp", "Bob Industries", "Charlie Ltd"]; + let mut created_ids = Vec::new(); + + for (i, name) in test_names.iter().enumerate() { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value(name)); + data.insert("kz".to_string(), create_string_value(&format!("KZ{}", i + 1))); + + if let Ok(response) = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await { + created_ids.push(response.inserted_id); + } + } + + if created_ids.len() < 3 { + println!("Could not create enough test records for positional test"); + return; + } + + println!("Created {} records with IDs: {:?}", created_ids.len(), created_ids); + + // 2. Test valid positional retrieval + for i in 0..3 { + let position = (i + 1) as i32; + let result = context.client.get_table_data_by_position( + context.profile_name.clone(), + context.table_name.clone(), + position, + ).await; + + match result { + Ok(response) => { + assert!(!response.data.is_empty(), "Position {} should return data", position); + if let Some(firma_value) = response.data.get("firma") { + assert!(test_names.contains(&firma_value.as_str()), + "Returned firma '{}' should be one of our test names", firma_value); + } + println!("Successfully retrieved record at position {}", position); + } + Err(e) => { + println!("Failed to get record at position {}: {}", position, e); + } + } + } + + // 3. Test out-of-bounds position + let oob_position = 100; + let result_oob = context.client.get_table_data_by_position( + context.profile_name.clone(), + context.table_name.clone(), + oob_position, + ).await; + + assert!(result_oob.is_err(), "Should fail for out-of-bounds position"); + if let Some(status) = result_oob.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::NotFound); + } + + // 4. Test invalid position (≤ 0) + let invalid_positions = [0, -1, -5]; + for invalid_pos in invalid_positions { + let result_invalid = context.client.get_table_data_by_position( + context.profile_name.clone(), + context.table_name.clone(), + invalid_pos, + ).await; + + assert!(result_invalid.is_err(), "Should fail for invalid position {}", invalid_pos); + if let Some(status) = result_invalid.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::InvalidArgument); + } + } +} + +// ======================================================================== +// WORKFLOW AND INTEGRATION TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_complete_crud_workflow(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let test_data = context.create_test_form_data(); + + // 1. CREATE - Post data + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + test_data.clone(), + ).await; + + let created_id = match post_result { + Ok(response) => { + assert!(response.success, "POST should succeed"); + println!("Workflow: Created record with ID {}", response.inserted_id); + response.inserted_id + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + if status.code() == tonic::Code::Unavailable { + println!("Workflow test skipped - backend not available"); + return; + } + } + panic!("Workflow POST failed unexpectedly: {}", e); + } + }; + + // 2. READ - Get the created data + let get_result = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ).await; + + let get_response = get_result.expect("Workflow GET should succeed"); + if let Some(firma_value) = get_response.data.get("firma") { + assert_eq!(firma_value, "Test Company Ltd", "Retrieved data should match created data"); + } + println!("Workflow: Verified created data"); + + // 3. UPDATE - Modify the data + let mut update_data = HashMap::new(); + update_data.insert("firma".to_string(), create_string_value("Updated in Workflow")); + update_data.insert("telefon".to_string(), create_string_value("+421999888777")); + + let put_result = context.client.put_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + update_data, + ).await; + + let put_response = put_result.expect("Workflow PUT should succeed"); + assert!(put_response.success, "PUT should succeed"); + assert_eq!(put_response.updated_id, created_id, "PUT should return correct ID"); + println!("Workflow: Updated record"); + + // 4. VERIFY UPDATE - Get updated data + let get_updated_result = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ).await; + + let get_updated_response = get_updated_result.expect("Workflow GET after update should succeed"); + if let Some(firma_value) = get_updated_response.data.get("firma") { + assert_eq!(firma_value, "Updated in Workflow", "Data should be updated"); + } + println!("Workflow: Verified updated data"); + + // 5. DELETE - Remove the data + let delete_result = context.client.delete_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ).await; + + let delete_response = delete_result.expect("Workflow DELETE should succeed"); + assert!(delete_response.success, "DELETE should succeed"); + println!("Workflow: Deleted record"); + + // 6. VERIFY DELETE - Ensure data is gone + let get_deleted_result = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ).await; + + assert!(get_deleted_result.is_err(), "Should not be able to GET deleted record"); + if let Some(status) = get_deleted_result.unwrap_err().downcast_ref::() { + assert_eq!(status.code(), tonic::Code::NotFound); + } + println!("Workflow: Verified record deletion"); + + println!("Complete CRUD workflow test successful for ID {}", created_id); +} + +// ======================================================================== +// FORM STATE INTEGRATION TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_form_state_integration(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Create a form state + let mut form_state = FormState::new( + context.profile_name.clone(), + context.table_name.clone(), + vec![], // columns would be populated in real use + ); + + // Test count update + let count_result = context.client.get_table_data_count( + context.profile_name.clone(), + context.table_name.clone(), + ).await; + + if let Ok(count) = count_result { + form_state.total_count = count; + assert_eq!(form_state.total_count, count, "Form state count should match backend"); + println!("Form state updated with count: {}", form_state.total_count); + } + + // Create a test record for form state testing + let test_data = context.create_test_form_data(); + if let Ok(post_response) = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + test_data, + ).await { + let created_id = post_response.inserted_id; + + // Test form state update from backend response + if let Ok(get_response) = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ).await { + form_state.update_from_response(&get_response.data, created_id as u64); + assert_eq!(form_state.current_position, created_id as u64, "Form state position should match"); + assert!(!form_state.has_unsaved_changes(), "Form state should not have unsaved changes after update"); + println!("Form state successfully updated from backend data"); + } + } else { + println!("Could not create test record for form state test"); + } +} + +// ======================================================================== +// CONCURRENT OPERATIONS TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_concurrent_post_operations(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Create multiple concurrent POST operations using tokio::spawn + let mut handles = Vec::new(); + + for i in 0..5 { + let context_clone = context.clone(); + let handle = tokio::spawn(async move { + let mut data = context_clone.create_test_form_data(); + data.insert("firma".to_string(), create_string_value(&format!("Concurrent Company {}", i))); + data.insert("kz".to_string(), create_string_value(&format!("CONC{}", i))); + + let mut client = context_clone.client; + client.post_table_data( + context_clone.profile_name.clone(), + context_clone.table_name.clone(), + data, + ).await + }); + handles.push(handle); + } + + // Wait for all tasks to complete + let mut success_count = 0; + for (i, handle) in handles.into_iter().enumerate() { + match handle.await { + Ok(Ok(response)) => { + assert!(response.success, "Concurrent POST {} should succeed", i); + success_count += 1; + } + Ok(Err(_)) => { + println!("Concurrent POST {} failed (may be expected if backend issues)", i); + } + Err(e) => { + println!("Concurrent task {} panicked: {}", i, e); + } + } + } + + println!("Concurrent operations: {}/{} succeeded", success_count, 5); + assert!(success_count > 0, "At least some concurrent operations should succeed"); +} + +// ======================================================================== +// PERFORMANCE AND STRESS TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_rapid_sequential_operations(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let start_time = std::time::Instant::now(); + let operation_count = 10; + let mut successful_operations = 0; + + for i in 0..operation_count { + let mut data = context.create_test_form_data(); + data.insert("firma".to_string(), create_string_value(&format!("Rapid Company {}", i))); + data.insert("kz".to_string(), create_string_value(&format!("RAP{}", i))); + + if let Ok(response) = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await { + assert!(response.success, "Rapid operation {} should succeed", i); + successful_operations += 1; + } + } + + let duration = start_time.elapsed(); + println!("{} rapid operations took: {:?}", operation_count, duration); + println!("Success rate: {}/{}", successful_operations, operation_count); + + assert!(successful_operations > 0, "At least some rapid operations should succeed"); + assert!(duration.as_secs() < 30, "Rapid operations should complete in reasonable time"); +} + +// ======================================================================== +// CONNECTION AND CLIENT TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_grpc_client_connection() { + if std::env::var("SKIP_BACKEND_TESTS").is_ok() { + println!("Connection test skipped due to SKIP_BACKEND_TESTS"); + return; + } + + let client_result = GrpcClient::new().await; + match client_result { + Ok(_) => println!("gRPC client connection test passed"), + Err(e) => { + println!("gRPC client connection failed (expected if backend not running): {}", e); + // Don't panic - this is expected when backend is not available + } + } +} + +#[rstest] +#[tokio::test] +async fn test_client_timeout_handling(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test that operations complete within reasonable timeouts + let timeout_duration = Duration::from_secs(10); + + let count_result = timeout( + timeout_duration, + context.client.get_table_data_count( + context.profile_name.clone(), + context.table_name.clone(), + ) + ).await; + + match count_result { + Ok(Ok(count)) => { + println!("Count operation completed within timeout: {}", count); + } + Ok(Err(e)) => { + println!("Count operation failed: {}", e); + } + Err(_) => { + panic!("Count operation timed out after {:?}", timeout_duration); + } + } +} + +// ======================================================================== +// DATA EDGE CASES TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_special_characters_and_unicode(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let special_strings = vec![ + "José María González", + "Москва", + "北京市", + "🚀 Tech Company 🌟", + "Quote\"Test'Apostrophe", + "Price: $1,000.50 (50% off!)", + ]; + + for (i, test_string) in special_strings.iter().enumerate() { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value(test_string)); + data.insert("kz".to_string(), create_string_value(&format!("UNI{}", i))); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + if let Ok(response) = result { + assert!(response.success, "Should handle special characters: '{}'", test_string); + println!("Successfully handled special string: '{}'", test_string); + } else { + println!("Failed to handle special string: '{}' (may be expected)", test_string); + } + } +} + +#[rstest] +#[tokio::test] +async fn test_null_and_empty_values(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value("Null Test Company")); + data.insert("telefon".to_string(), create_null_value()); + data.insert("email".to_string(), create_string_value("")); + data.insert("ulica".to_string(), create_string_value(" ")); // Whitespace only + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + if let Ok(response) = result { + assert!(response.success, "Should handle null and empty values"); + println!("Successfully handled null and empty values"); + } else { + println!("Failed to handle null and empty values (may be expected based on validation)"); + } +} + include!("form_request_tests2.rs"); diff --git a/client/tests/form/requests/form_request_tests2.rs b/client/tests/form/requests/form_request_tests2.rs index d1837a0..0dd9507 100644 --- a/client/tests/form/requests/form_request_tests2.rs +++ b/client/tests/form/requests/form_request_tests2.rs @@ -1,859 +1,267 @@ -#[rstest] -#[tokio::test] -async fn test_post_table_data_success(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let test_data = context.create_test_form_data(); - - let result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - test_data.clone(), - ).await; - - match result { - Ok(response) => { - assert!(response.success, "POST operation should succeed"); - assert!(response.inserted_id > 0, "Should return valid inserted ID"); - assert!(!response.message.is_empty(), "Should return non-empty message"); - println!("POST successful: ID {}, Message: {}", response.inserted_id, response.message); - } - Err(e) => { - if let Some(status) = e.downcast_ref::() { - if status.code() == tonic::Code::Unavailable { - println!("Backend unavailable - test cannot run"); - return; - } - } - panic!("POST request failed unexpectedly: {}", e); - } - } -} +// ======================================================================== +// ROBUST WORKFLOW AND INTEGRATION TESTS +// ======================================================================== #[rstest] #[tokio::test] -async fn test_post_table_data_minimal_data(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let minimal_data = context.create_minimal_form_data(); - - let result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - minimal_data, - ).await; - - assert!(result.is_ok(), "POST with minimal data should succeed"); - let response = result.unwrap(); - assert!(response.success); - assert!(response.inserted_id > 0); -} - -#[rstest] -#[tokio::test] -async fn test_get_table_data_count_success(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let result = context.client.get_table_data_count( - context.profile_name.clone(), - context.table_name.clone(), - ).await; - - match result { - Ok(count) => { - assert!(count >= 0, "Count should be non-negative"); - println!("GET count successful: {}", count); - } - Err(e) => { - if let Some(status) = e.downcast_ref::() { - if status.code() == tonic::Code::Unavailable { - println!("Backend unavailable - test cannot run"); - return; - } - } - panic!("GET count request failed unexpectedly: {}", e); - } - } -} - -#[rstest] -#[tokio::test] -async fn test_get_table_data_by_id_with_existing_record(#[future] populated_test_context: FormTestContext) { +async fn test_partial_update_preserves_other_fields( + #[future] populated_test_context: FormTestContext, +) { let mut context = populated_test_context.await; skip_if_backend_unavailable!(); - // First create a record - let test_data = context.create_test_form_data(); - let post_result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - test_data.clone(), - ).await; - - if let Ok(post_response) = post_result { - let created_id = post_response.inserted_id; - - // Now try to get it - let get_result = context.client.get_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - ).await; - - match get_result { - Ok(response) => { - assert!(!response.data.is_empty(), "Should return data fields"); - println!("GET by ID successful: {} fields", response.data.len()); - - // Verify some data matches - if let Some(firma_value) = response.data.get("firma") { - assert_eq!(firma_value, "Test Company Ltd"); - } - } - Err(e) => panic!("GET by ID failed unexpectedly: {}", e), - } - } else { - println!("Could not create test record, skipping GET test"); - } -} - -#[rstest] -#[tokio::test] -async fn test_get_table_data_by_nonexistent_id(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let nonexistent_id = 99999; - let result = context.client.get_table_data( - context.profile_name.clone(), - context.table_name.clone(), - nonexistent_id, - ).await; - - assert!(result.is_err(), "GET should fail for nonexistent ID"); - if let Some(status) = result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::NotFound); - } -} - -#[rstest] -#[tokio::test] -async fn test_put_table_data_success(#[future] populated_test_context: FormTestContext) { - let mut context = populated_test_context.await; - skip_if_backend_unavailable!(); - - // First create a record - let test_data = context.create_test_form_data(); - let post_result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - test_data, - ).await; - - if let Ok(post_response) = post_result { - let created_id = post_response.inserted_id; - - // Update the record - let mut update_data = HashMap::new(); - update_data.insert("firma".to_string(), create_string_value("Updated Company Name")); - update_data.insert("telefon".to_string(), create_string_value("+421987654321")); - - let put_result = context.client.put_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - update_data, - ).await; - - match put_result { - Ok(response) => { - assert!(response.success, "PUT operation should succeed"); - assert_eq!(response.updated_id, created_id, "Should return correct updated ID"); - println!("PUT successful: ID {}, Message: {}", response.updated_id, response.message); - } - Err(e) => panic!("PUT request failed unexpectedly: {}", e), - } - } else { - println!("Could not create test record, skipping PUT test"); - } -} - -#[rstest] -#[tokio::test] -async fn test_put_table_data_nonexistent_id(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let nonexistent_id = 99999; - let mut update_data = HashMap::new(); - update_data.insert("firma".to_string(), create_string_value("Updated Company")); - - let result = context.client.put_table_data( - context.profile_name.clone(), - context.table_name.clone(), - nonexistent_id, - update_data, - ).await; - - assert!(result.is_err(), "PUT should fail for nonexistent ID"); - if let Some(status) = result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::NotFound); - } -} - -#[rstest] -#[tokio::test] -async fn test_delete_table_data_success(#[future] populated_test_context: FormTestContext) { - let mut context = populated_test_context.await; - skip_if_backend_unavailable!(); - - // First create a record - let test_data = context.create_test_form_data(); - let post_result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - test_data, - ).await; - - if let Ok(post_response) = post_result { - let created_id = post_response.inserted_id; - - let delete_result = context.client.delete_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - ).await; - - match delete_result { - Ok(response) => { - assert!(response.success, "DELETE operation should succeed"); - println!("DELETE successful for ID {}", created_id); - } - Err(e) => panic!("DELETE request failed unexpectedly: {}", e), - } - } else { - println!("Could not create test record, skipping DELETE test"); - } -} - -#[rstest] -#[tokio::test] -async fn test_delete_table_data_nonexistent_id(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let nonexistent_id = 99999; - let result = context.client.delete_table_data( - context.profile_name.clone(), - context.table_name.clone(), - nonexistent_id, - ).await; - - // DELETE should succeed even for nonexistent IDs (idempotent operation) - assert!(result.is_ok(), "DELETE should not fail for nonexistent ID"); - let response = result.unwrap(); - assert!(response.success, "DELETE should report success even for nonexistent ID"); -} - -// ======================================================================== -// ERROR HANDLING AND VALIDATION TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_invalid_profile_and_table_errors(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let bogus_profile = "profile_does_not_exist".to_string(); - let bogus_table = "table_does_not_exist".to_string(); - - let result = context.client.get_table_data_count(bogus_profile, bogus_table).await; - - assert!(result.is_err(), "Expected error for non-existent profile/table"); - if let Some(status) = result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::NotFound, "Expected NotFound for non-existent profile"); - } -} - -#[rstest] -#[tokio::test] -async fn test_invalid_column_validation(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let invalid_data = context.create_invalid_form_data(); - - let result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - invalid_data, - ).await; - - assert!(result.is_err(), "Expected error for undefined column"); - if let Some(status) = result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::InvalidArgument); - assert!(status.message().contains("Invalid column") || - status.message().contains("nonexistent")); - } -} - -#[rstest] -#[tokio::test] -async fn test_data_type_validation(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let type_mismatch_data = context.create_type_mismatch_data(); - - let result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - type_mismatch_data, - ).await; - - assert!(result.is_err(), "Expected error for wrong data type"); - if let Some(status) = result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::InvalidArgument); - } -} - -#[rstest] -#[tokio::test] -async fn test_empty_data_validation(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let empty_data = HashMap::new(); - - let result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - empty_data, - ).await; - - assert!(result.is_err(), "Expected error for empty data"); - if let Some(status) = result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::InvalidArgument); - } -} - -// ======================================================================== -// SOFT DELETE BEHAVIOR TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_soft_delete_behavior_comprehensive(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - // 1. Create a record - let test_data = context.create_test_form_data(); - let post_result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - test_data, - ).await; - - if let Ok(post_response) = post_result { - let record_id = post_response.inserted_id; - println!("Created record with ID {}", record_id); - - // 2. Verify count before deletion - let count_before = context.client.get_table_data_count( - context.profile_name.clone(), - context.table_name.clone() - ).await.unwrap_or(0); - assert!(count_before >= 1, "Count should be at least 1 after creation"); - - // 3. Soft-delete the record - let delete_result = context.client.delete_table_data( - context.profile_name.clone(), - context.table_name.clone(), - record_id, - ).await; - - assert!(delete_result.is_ok(), "Delete operation should succeed"); - println!("Soft-deleted record {}", record_id); - - // 4. Verify count decreased after deletion - let count_after = context.client.get_table_data_count( - context.profile_name.clone(), - context.table_name.clone() - ).await.unwrap_or(0); - assert_eq!(count_after, count_before - 1, "Count should decrease by 1 after soft delete"); - - // 5. Try to GET the soft-deleted record - let get_result = context.client.get_table_data( - context.profile_name.clone(), - context.table_name.clone(), - record_id, - ).await; - - assert!(get_result.is_err(), "Should not be able to GET a soft-deleted record"); - if let Some(status) = get_result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::NotFound); - } - println!("Correctly failed to GET soft-deleted record"); - } else { - println!("Could not create test record for soft delete test"); - } -} - -// ======================================================================== -// POSITIONAL RETRIEVAL TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_positional_retrieval_comprehensive(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - // 1. Create multiple records - let test_names = ["Alice Corp", "Bob Industries", "Charlie Ltd"]; - let mut created_ids = Vec::new(); - - for (i, name) in test_names.iter().enumerate() { - let mut data = HashMap::new(); - data.insert("firma".to_string(), create_string_value(name)); - data.insert("kz".to_string(), create_string_value(&format!("KZ{}", i + 1))); - - if let Ok(response) = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - data, - ).await { - created_ids.push(response.inserted_id); - } - } - - if created_ids.len() < 3 { - println!("Could not create enough test records for positional test"); - return; - } - - println!("Created {} records with IDs: {:?}", created_ids.len(), created_ids); - - // 2. Test valid positional retrieval - for i in 0..3 { - let position = (i + 1) as i32; - let result = context.client.get_table_data_by_position( - context.profile_name.clone(), - context.table_name.clone(), - position, - ).await; - - match result { - Ok(response) => { - assert!(!response.data.is_empty(), "Position {} should return data", position); - if let Some(firma_value) = response.data.get("firma") { - assert!(test_names.contains(&firma_value.as_str()), - "Returned firma '{}' should be one of our test names", firma_value); - } - println!("Successfully retrieved record at position {}", position); - } - Err(e) => { - println!("Failed to get record at position {}: {}", position, e); - } - } - } - - // 3. Test out-of-bounds position - let oob_position = 100; - let result_oob = context.client.get_table_data_by_position( - context.profile_name.clone(), - context.table_name.clone(), - oob_position, - ).await; - - assert!(result_oob.is_err(), "Should fail for out-of-bounds position"); - if let Some(status) = result_oob.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::NotFound); - } - - // 4. Test invalid position (≤ 0) - let invalid_positions = [0, -1, -5]; - for invalid_pos in invalid_positions { - let result_invalid = context.client.get_table_data_by_position( - context.profile_name.clone(), - context.table_name.clone(), - invalid_pos, - ).await; - - assert!(result_invalid.is_err(), "Should fail for invalid position {}", invalid_pos); - if let Some(status) = result_invalid.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::InvalidArgument); - } - } -} - -// ======================================================================== -// WORKFLOW AND INTEGRATION TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_complete_crud_workflow(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let test_data = context.create_test_form_data(); - - // 1. CREATE - Post data - let post_result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - test_data.clone(), - ).await; - - let created_id = match post_result { - Ok(response) => { - assert!(response.success, "POST should succeed"); - println!("Workflow: Created record with ID {}", response.inserted_id); - response.inserted_id - } - Err(e) => { - if let Some(status) = e.downcast_ref::() { - if status.code() == tonic::Code::Unavailable { - println!("Workflow test skipped - backend not available"); - return; - } - } - panic!("Workflow POST failed unexpectedly: {}", e); - } - }; - - // 2. READ - Get the created data - let get_result = context.client.get_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - ).await; - - let get_response = get_result.expect("Workflow GET should succeed"); - if let Some(firma_value) = get_response.data.get("firma") { - assert_eq!(firma_value, "Test Company Ltd", "Retrieved data should match created data"); - } - println!("Workflow: Verified created data"); - - // 3. UPDATE - Modify the data - let mut update_data = HashMap::new(); - update_data.insert("firma".to_string(), create_string_value("Updated in Workflow")); - update_data.insert("telefon".to_string(), create_string_value("+421999888777")); - - let put_result = context.client.put_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - update_data, - ).await; - - let put_response = put_result.expect("Workflow PUT should succeed"); - assert!(put_response.success, "PUT should succeed"); - assert_eq!(put_response.updated_id, created_id, "PUT should return correct ID"); - println!("Workflow: Updated record"); - - // 4. VERIFY UPDATE - Get updated data - let get_updated_result = context.client.get_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - ).await; - - let get_updated_response = get_updated_result.expect("Workflow GET after update should succeed"); - if let Some(firma_value) = get_updated_response.data.get("firma") { - assert_eq!(firma_value, "Updated in Workflow", "Data should be updated"); - } - println!("Workflow: Verified updated data"); - - // 5. DELETE - Remove the data - let delete_result = context.client.delete_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - ).await; - - let delete_response = delete_result.expect("Workflow DELETE should succeed"); - assert!(delete_response.success, "DELETE should succeed"); - println!("Workflow: Deleted record"); - - // 6. VERIFY DELETE - Ensure data is gone - let get_deleted_result = context.client.get_table_data( - context.profile_name.clone(), - context.table_name.clone(), - created_id, - ).await; - - assert!(get_deleted_result.is_err(), "Should not be able to GET deleted record"); - if let Some(status) = get_deleted_result.unwrap_err().downcast_ref::() { - assert_eq!(status.code(), tonic::Code::NotFound); - } - println!("Workflow: Verified record deletion"); - - println!("Complete CRUD workflow test successful for ID {}", created_id); -} - -// ======================================================================== -// FORM STATE INTEGRATION TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_form_state_integration(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - // Create a form state - let mut form_state = FormState::new( - context.profile_name.clone(), - context.table_name.clone(), - vec![], // columns would be populated in real use + // 1. Create a record with multiple fields + let mut initial_data = context.create_test_form_data(); + let original_email = "preserve.this@email.com"; + initial_data.insert( + "email".to_string(), + create_string_value(original_email), ); - // Test count update - let count_result = context.client.get_table_data_count( - context.profile_name.clone(), - context.table_name.clone(), - ).await; + let post_res = context + .client + .post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + initial_data, + ) + .await + .expect("Setup: Failed to create record for partial update test"); + let created_id = post_res.inserted_id; + println!("Partial Update Test: Created record ID {}", created_id); - if let Ok(count) = count_result { - form_state.total_count = count; - assert_eq!(form_state.total_count, count, "Form state count should match backend"); - println!("Form state updated with count: {}", form_state.total_count); - } + // 2. Update only ONE field + let mut partial_update = HashMap::new(); + let updated_firma = "Partially Updated Inc."; + partial_update.insert( + "firma".to_string(), + create_string_value(updated_firma), + ); - // Create a test record for form state testing - let test_data = context.create_test_form_data(); - if let Ok(post_response) = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - test_data, - ).await { - let created_id = post_response.inserted_id; - - // Test form state update from backend response - if let Ok(get_response) = context.client.get_table_data( + context + .client + .put_table_data( context.profile_name.clone(), context.table_name.clone(), created_id, - ).await { - form_state.update_from_response(&get_response.data, created_id as u64); - assert_eq!(form_state.current_position, created_id as u64, "Form state position should match"); - assert!(!form_state.has_unsaved_changes(), "Form state should not have unsaved changes after update"); - println!("Form state successfully updated from backend data"); - } - } else { - println!("Could not create test record for form state test"); - } -} + partial_update, + ) + .await + .expect("Partial update failed"); + println!("Partial Update Test: Updated only 'firma' field"); -// ======================================================================== -// CONCURRENT OPERATIONS TESTS -// ======================================================================== + // 3. Get the record back and verify ALL fields + let get_res = context + .client + .get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ) + .await + .expect("Failed to get record after partial update"); + + let final_data = get_res.data; + assert_eq!( + final_data.get("firma").unwrap(), + updated_firma, + "The 'firma' field should be updated" + ); + assert_eq!( + final_data.get("email").unwrap(), + original_email, + "The 'email' field should have been preserved" + ); + println!("Partial Update Test: Verified other fields were preserved. OK."); +} #[rstest] #[tokio::test] -async fn test_concurrent_post_operations(#[future] form_test_context: FormTestContext) { +async fn test_data_edge_cases_and_unicode( + #[future] form_test_context: FormTestContext, +) { let mut context = form_test_context.await; skip_if_backend_unavailable!(); - // Create multiple concurrent POST operations using tokio::spawn - let mut handles = Vec::new(); - - for i in 0..5 { - let context_clone = context.clone(); - let handle = tokio::spawn(async move { - let mut data = context_clone.create_test_form_data(); - data.insert("firma".to_string(), create_string_value(&format!("Concurrent Company {}", i))); - data.insert("kz".to_string(), create_string_value(&format!("CONC{}", i))); + let edge_case_strings = vec![ + ("Unicode", "José María González, Москва, 北京市"), + ("Emoji", "🚀 Tech Company 🌟"), + ("Quotes", "Quote\"Test'Apostrophe"), + ("Symbols", "Price: $1,000.50 (50% off!)"), + ("Empty", ""), + ("Whitespace", " "), + ]; - let mut client = context_clone.client; - client.post_table_data( - context_clone.profile_name.clone(), - context_clone.table_name.clone(), + for (case_name, test_string) in edge_case_strings { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value(test_string)); + data.insert( + "kz".to_string(), + create_string_value(&format!("EDGE-{}", case_name)), + ); + + let post_res = context + .client + .post_table_data( + context.profile_name.clone(), + context.table_name.clone(), data, - ).await + ) + .await + .expect(&format!("POST should succeed for case: {}", case_name)); + let created_id = post_res.inserted_id; + + let get_res = context + .client + .get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + created_id, + ) + .await + .expect(&format!( + "GET should succeed for case: {}", + case_name + )); + + assert_eq!( + get_res.data.get("firma").unwrap(), + test_string, + "Data should be identical after round-trip for case: {}", + case_name + ); + println!("Edge Case Test: '{}' passed.", case_name); + } +} + +#[rstest] +#[tokio::test] +async fn test_numeric_and_null_edge_cases( + #[future] form_test_context: FormTestContext, +) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Test NULL value + let mut null_data = HashMap::new(); + null_data.insert( + "firma".to_string(), + create_string_value("Company With Null Phone"), + ); + null_data.insert("telefon".to_string(), create_null_value()); + let post_res_null = context + .client + .post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + null_data, + ) + .await + .expect("POST with NULL value should succeed"); + let get_res_null = context + .client + .get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + post_res_null.inserted_id, + ) + .await + .unwrap(); + // Depending on DB, NULL may come back as empty string or be absent. + // The important part is that the operation doesn't fail. + assert!( + get_res_null.data.get("telefon").unwrap_or(&"".to_string()).is_empty(), + "NULL value should result in an empty or absent field" + ); + println!("Edge Case Test: NULL value handled correctly. OK."); + + // 2. Test Zero value for a numeric field (assuming 'age' is numeric) + let mut zero_data = HashMap::new(); + zero_data.insert( + "firma".to_string(), + create_string_value("Newborn Company"), + ); + // Assuming 'age' is a field in your actual table definition + // zero_data.insert("age".to_string(), create_number_value(0.0)); + // let post_res_zero = context.client.post_table_data(...).await.expect("POST with zero should succeed"); + // ... then get and verify it's "0" + println!("Edge Case Test: Zero value test skipped (uncomment if 'age' field exists)."); +} + +#[rstest] +#[tokio::test] +async fn test_concurrent_updates_on_same_record( + #[future] populated_test_context: FormTestContext, +) { + let mut context = populated_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Create a single record to be updated by all tasks + let initial_data = context.create_minimal_form_data(); + let post_res = context + .client + .post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + initial_data, + ) + .await + .expect("Setup: Failed to create record for concurrency test"); + let record_id = post_res.inserted_id; + println!("Concurrency Test: Target record ID is {}", record_id); + + // 2. Spawn multiple concurrent UPDATE operations + let mut handles = Vec::new(); + let num_concurrent_tasks = 5; + let mut final_values = Vec::new(); + + for i in 0..num_concurrent_tasks { + let mut client_clone = context.client.clone(); + let profile_name = context.profile_name.clone(); + let table_name = context.table_name.clone(); + let final_value = format!("Concurrent Update {}", i); + final_values.push(final_value.clone()); + + let handle = tokio::spawn(async move { + let mut update_data = HashMap::new(); + update_data.insert( + "firma".to_string(), + create_string_value(&final_value), + ); + client_clone + .put_table_data(profile_name, table_name, record_id, update_data) + .await }); handles.push(handle); } - // Wait for all tasks to complete - let mut success_count = 0; - for (i, handle) in handles.into_iter().enumerate() { - match handle.await { - Ok(Ok(response)) => { - assert!(response.success, "Concurrent POST {} should succeed", i); - success_count += 1; - } - Ok(Err(_)) => { - println!("Concurrent POST {} failed (may be expected if backend issues)", i); - } - Err(e) => { - println!("Concurrent task {} panicked: {}", i, e); - } - } - } + // 3. Wait for all tasks to complete and check for panics + let results = futures::future::join_all(handles).await; + assert!( + results.iter().all(|r| r.is_ok()), + "No concurrent task should panic" + ); + println!("Concurrency Test: All update tasks completed without panicking."); - println!("Concurrent operations: {}/{} succeeded", success_count, 5); - assert!(success_count > 0, "At least some concurrent operations should succeed"); -} - -// ======================================================================== -// PERFORMANCE AND STRESS TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_rapid_sequential_operations(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let start_time = std::time::Instant::now(); - let operation_count = 10; - let mut successful_operations = 0; - - for i in 0..operation_count { - let mut data = context.create_test_form_data(); - data.insert("firma".to_string(), create_string_value(&format!("Rapid Company {}", i))); - data.insert("kz".to_string(), create_string_value(&format!("RAP{}", i))); - - if let Ok(response) = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - data, - ).await { - assert!(response.success, "Rapid operation {} should succeed", i); - successful_operations += 1; - } - } - - let duration = start_time.elapsed(); - println!("{} rapid operations took: {:?}", operation_count, duration); - println!("Success rate: {}/{}", successful_operations, operation_count); - - assert!(successful_operations > 0, "At least some rapid operations should succeed"); - assert!(duration.as_secs() < 30, "Rapid operations should complete in reasonable time"); -} - -// ======================================================================== -// CONNECTION AND CLIENT TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_grpc_client_connection() { - if std::env::var("SKIP_BACKEND_TESTS").is_ok() { - println!("Connection test skipped due to SKIP_BACKEND_TESTS"); - return; - } - - let client_result = GrpcClient::new().await; - match client_result { - Ok(_) => println!("gRPC client connection test passed"), - Err(e) => { - println!("gRPC client connection failed (expected if backend not running): {}", e); - // Don't panic - this is expected when backend is not available - } - } -} - -#[rstest] -#[tokio::test] -async fn test_client_timeout_handling(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - // Test that operations complete within reasonable timeouts - let timeout_duration = Duration::from_secs(10); - - let count_result = timeout( - timeout_duration, - context.client.get_table_data_count( + // 4. Get the final state of the record + let final_get_res = context + .client + .get_table_data( context.profile_name.clone(), context.table_name.clone(), + record_id, ) - ).await; + .await + .expect("Should be able to get the record after concurrent updates"); - match count_result { - Ok(Ok(count)) => { - println!("Count operation completed within timeout: {}", count); - } - Ok(Err(e)) => { - println!("Count operation failed: {}", e); - } - Err(_) => { - panic!("Count operation timed out after {:?}", timeout_duration); - } - } + let final_firma = final_get_res.data.get("firma").unwrap(); + assert!( + final_values.contains(final_firma), + "The final state '{}' must be one of the states set by the tasks", + final_firma + ); + println!( + "Concurrency Test: Final state is '{}', which is a valid outcome. OK.", + final_firma + ); } - -// ======================================================================== -// DATA EDGE CASES TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_special_characters_and_unicode(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let special_strings = vec![ - "José María González", - "Москва", - "北京市", - "🚀 Tech Company 🌟", - "Quote\"Test'Apostrophe", - "Price: $1,000.50 (50% off!)", - ]; - - for (i, test_string) in special_strings.iter().enumerate() { - let mut data = HashMap::new(); - data.insert("firma".to_string(), create_string_value(test_string)); - data.insert("kz".to_string(), create_string_value(&format!("UNI{}", i))); - - let result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - data, - ).await; - - if let Ok(response) = result { - assert!(response.success, "Should handle special characters: '{}'", test_string); - println!("Successfully handled special string: '{}'", test_string); - } else { - println!("Failed to handle special string: '{}' (may be expected)", test_string); - } - } -} - -#[rstest] -#[tokio::test] -async fn test_null_and_empty_values(#[future] form_test_context: FormTestContext) { - let mut context = form_test_context.await; - skip_if_backend_unavailable!(); - - let mut data = HashMap::new(); - data.insert("firma".to_string(), create_string_value("Null Test Company")); - data.insert("telefon".to_string(), create_null_value()); - data.insert("email".to_string(), create_string_value("")); - data.insert("ulica".to_string(), create_string_value(" ")); // Whitespace only - - let result = context.client.post_table_data( - context.profile_name.clone(), - context.table_name.clone(), - data, - ).await; - - if let Ok(response) = result { - assert!(response.success, "Should handle null and empty values"); - println!("Successfully handled null and empty values"); - } else { - println!("Failed to handle null and empty values (may be expected based on validation)"); - } -} -