diff --git a/client/tests/form/requests/form_request_tests.rs b/client/tests/form/requests/form_request_tests.rs index 06c1ace..3e03bb6 100644 --- a/client/tests/form/requests/form_request_tests.rs +++ b/client/tests/form/requests/form_request_tests.rs @@ -1,14 +1,14 @@ // client/tests/form_request_tests.rs -use rstest::{fixture, rstest}; -use client::services::grpc_client::GrpcClient; -use client::state::pages::form::FormState; -use client::state::pages::canvas_state::CanvasState; -use prost_types::Value; -use prost_types::value::Kind; -use std::collections::HashMap; -use tonic::Status; -use tokio::time::{timeout, Duration}; -use std::time::{SystemTime, UNIX_EPOCH}; +pub use rstest::{fixture, rstest}; +pub use client::services::grpc_client::GrpcClient; +pub use client::state::pages::form::FormState; +pub use client::state::pages::canvas_state::CanvasState; +pub use prost_types::Value; +pub use prost_types::value::Kind; +pub use std::collections::HashMap; +pub use tonic::Status; +pub use tokio::time::{timeout, Duration}; +pub use std::time::{SystemTime, UNIX_EPOCH}; // ======================================================================== // HELPER FUNCTIONS AND UTILITIES @@ -1016,3 +1016,4 @@ async fn test_null_and_empty_values(#[future] form_test_context: FormTestContext } include!("form_request_tests2.rs"); +// include!("form_request_tests3.rs"); diff --git a/client/tests/form/requests/form_request_tests3.rs b/client/tests/form/requests/form_request_tests3.rs new file mode 100644 index 0000000..cdebce4 --- /dev/null +++ b/client/tests/form/requests/form_request_tests3.rs @@ -0,0 +1,728 @@ +// form_request_tests3.rs - Comprehensive and Robust Testing +use super::*; + +// ======================================================================== +// STEEL SCRIPT VALIDATION TESTS (HIGHEST PRIORITY) +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_steel_script_validation_success(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test with data that should pass script validation + // Assuming there's a script that validates 'kz' field to start with "KZ" and be 5 chars + let mut valid_data = HashMap::new(); + valid_data.insert("firma".to_string(), create_string_value("Script Test Company")); + valid_data.insert("kz".to_string(), create_string_value("KZ123")); + valid_data.insert("telefon".to_string(), create_string_value("+421123456789")); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + valid_data, + ).await; + + match result { + Ok(response) => { + assert!(response.success, "Valid data should pass script validation"); + println!("Script Validation Test: Valid data passed - ID {}", response.inserted_id); + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + if status.code() == tonic::Code::Unavailable { + println!("Script validation test skipped - backend not available"); + return; + } + // If there are no scripts configured, this might still work + println!("Script validation test: {}", status.message()); + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_steel_script_validation_failure(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test with data that should fail script validation + let invalid_script_data = vec![ + ("TooShort", "KZ12"), // Too short + ("TooLong", "KZ12345"), // Too long + ("WrongPrefix", "AB123"), // Wrong prefix + ("NoPrefix", "12345"), // No prefix + ("Empty", ""), // Empty + ]; + + for (test_case, invalid_kz) in invalid_script_data { + let mut invalid_data = HashMap::new(); + invalid_data.insert("firma".to_string(), create_string_value("Script Fail Company")); + invalid_data.insert("kz".to_string(), create_string_value(invalid_kz)); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + invalid_data, + ).await; + + match result { + Ok(_) => { + println!("Script Validation Test: {} passed (no validation script configured)", test_case); + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + assert_eq!(status.code(), tonic::Code::InvalidArgument, + "Script validation failure should return InvalidArgument for case: {}", test_case); + println!("Script Validation Test: {} correctly failed - {}", test_case, status.message()); + } + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_steel_script_validation_on_update(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Create a valid record first + let mut initial_data = HashMap::new(); + initial_data.insert("firma".to_string(), create_string_value("Update Script Test")); + initial_data.insert("kz".to_string(), create_string_value("KZ123")); + + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + initial_data, + ).await; + + if let Ok(post_response) = post_result { + let record_id = post_response.inserted_id; + + // 2. Try to update with invalid data + let mut invalid_update = HashMap::new(); + invalid_update.insert("kz".to_string(), create_string_value("INVALID")); + + let update_result = context.client.put_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + invalid_update, + ).await; + + match update_result { + Ok(_) => { + println!("Script Validation on Update: No validation script configured for updates"); + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + assert_eq!(status.code(), tonic::Code::InvalidArgument, + "Update with invalid data should fail script validation"); + println!("Script Validation on Update: Correctly rejected invalid update"); + } + } + } + } +} + +// ======================================================================== +// COMPREHENSIVE DATA TYPE TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_boolean_data_type(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test valid boolean values + let boolean_test_cases = vec![ + ("true", true), + ("false", false), + ]; + + for (case_name, bool_value) in boolean_test_cases { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value("Boolean Test Company")); + // Assuming there's a boolean field called 'active' + data.insert("active".to_string(), create_bool_value(bool_value)); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + match result { + Ok(response) => { + println!("Boolean Test: {} value succeeded", case_name); + + // Verify the value round-trip + if let Ok(get_response) = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + response.inserted_id, + ).await { + if let Some(retrieved_value) = get_response.data.get("active") { + println!("Boolean Test: {} round-trip value: {}", case_name, retrieved_value); + } + } + } + Err(e) => { + println!("Boolean Test: {} failed (field may not exist): {}", case_name, e); + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_numeric_data_types(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test various numeric values + let numeric_test_cases = vec![ + ("Zero", 0.0), + ("Positive", 123.45), + ("Negative", -67.89), + ("Large", 999999.99), + ("SmallDecimal", 0.01), + ]; + + for (case_name, numeric_value) in numeric_test_cases { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value("Numeric Test Company")); + // Assuming there's a numeric field called 'price' or 'amount' + data.insert("amount".to_string(), create_number_value(numeric_value)); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + match result { + Ok(response) => { + println!("Numeric Test: {} ({}) succeeded", case_name, numeric_value); + + // Verify round-trip + if let Ok(get_response) = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + response.inserted_id, + ).await { + if let Some(retrieved_value) = get_response.data.get("amount") { + println!("Numeric Test: {} round-trip value: {}", case_name, retrieved_value); + } + } + } + Err(e) => { + println!("Numeric Test: {} failed (field may not exist): {}", case_name, e); + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_timestamp_data_type(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test various timestamp formats + let timestamp_test_cases = vec![ + ("ISO8601", "2024-01-15T10:30:00Z"), + ("WithTimezone", "2024-01-15T10:30:00+01:00"), + ("WithMilliseconds", "2024-01-15T10:30:00.123Z"), + ]; + + for (case_name, timestamp_str) in timestamp_test_cases { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value("Timestamp Test Company")); + // Assuming there's a timestamp field called 'created_at' + data.insert("created_at".to_string(), create_string_value(timestamp_str)); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + match result { + Ok(response) => { + println!("Timestamp Test: {} succeeded", case_name); + + // Verify round-trip + if let Ok(get_response) = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + response.inserted_id, + ).await { + if let Some(retrieved_value) = get_response.data.get("created_at") { + println!("Timestamp Test: {} round-trip value: {}", case_name, retrieved_value); + } + } + } + Err(e) => { + println!("Timestamp Test: {} failed (field may not exist): {}", case_name, e); + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_invalid_data_types(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test invalid data type combinations + let invalid_type_cases = vec![ + ("StringForNumber", "amount", create_string_value("not-a-number")), + ("NumberForBoolean", "active", create_number_value(123.0)), + ("StringForBoolean", "active", create_string_value("maybe")), + ("InvalidTimestamp", "created_at", create_string_value("not-a-date")), + ]; + + for (case_name, field_name, invalid_value) in invalid_type_cases { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value("Invalid Type Test")); + data.insert(field_name.to_string(), invalid_value); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + match result { + Ok(_) => { + println!("Invalid Type Test: {} passed (no type validation or field doesn't exist)", case_name); + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + assert_eq!(status.code(), tonic::Code::InvalidArgument, + "Invalid data type should return InvalidArgument for case: {}", case_name); + println!("Invalid Type Test: {} correctly rejected - {}", case_name, status.message()); + } + } + } + } +} + +// ======================================================================== +// FOREIGN KEY RELATIONSHIP TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_foreign_key_valid_relationship(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Create a parent record first (e.g., company) + let mut parent_data = HashMap::new(); + parent_data.insert("firma".to_string(), create_string_value("Parent Company")); + + let parent_result = context.client.post_table_data( + context.profile_name.clone(), + "companies".to_string(), // Assuming companies table exists + parent_data, + ).await; + + if let Ok(parent_response) = parent_result { + let parent_id = parent_response.inserted_id; + + // 2. Create a child record that references the parent + let mut child_data = HashMap::new(); + child_data.insert("name".to_string(), create_string_value("Child Record")); + child_data.insert("company_id".to_string(), create_number_value(parent_id as f64)); + + let child_result = context.client.post_table_data( + context.profile_name.clone(), + "contacts".to_string(), // Assuming contacts table exists + child_data, + ).await; + + match child_result { + Ok(child_response) => { + assert!(child_response.success, "Valid foreign key relationship should succeed"); + println!("Foreign Key Test: Valid relationship created - Parent ID: {}, Child ID: {}", + parent_id, child_response.inserted_id); + } + Err(e) => { + println!("Foreign Key Test: Failed (tables may not exist or no FK constraint): {}", e); + } + } + } else { + println!("Foreign Key Test: Could not create parent record"); + } +} + +#[rstest] +#[tokio::test] +async fn test_foreign_key_invalid_relationship(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Try to create a child record with non-existent parent ID + let mut invalid_child_data = HashMap::new(); + invalid_child_data.insert("name".to_string(), create_string_value("Orphan Record")); + invalid_child_data.insert("company_id".to_string(), create_number_value(99999.0)); // Non-existent ID + + let result = context.client.post_table_data( + context.profile_name.clone(), + "contacts".to_string(), + invalid_child_data, + ).await; + + match result { + Ok(_) => { + println!("Foreign Key Test: Invalid relationship passed (no FK constraint configured)"); + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + // Could be InvalidArgument or NotFound depending on implementation + assert!(matches!(status.code(), tonic::Code::InvalidArgument | tonic::Code::NotFound), + "Invalid foreign key should return InvalidArgument or NotFound"); + println!("Foreign Key Test: Invalid relationship correctly rejected - {}", status.message()); + } + } + } +} + +// ======================================================================== +// DELETED RECORD INTERACTION TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_update_deleted_record_behavior(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Create a record + let initial_data = context.create_test_form_data(); + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + initial_data, + ).await; + + if let Ok(post_response) = post_result { + let record_id = post_response.inserted_id; + println!("Deleted Record Test: Created record ID {}", record_id); + + // 2. Delete the record (soft delete) + 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 should succeed"); + println!("Deleted Record Test: Soft-deleted record {}", record_id); + + // 3. Try to UPDATE the deleted record + let mut update_data = HashMap::new(); + update_data.insert("firma".to_string(), create_string_value("Updated Deleted Record")); + + let update_result = context.client.put_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + update_data, + ).await; + + match update_result { + Ok(_) => { + // This might be a bug - updating deleted records should probably fail + println!("Deleted Record Test: UPDATE on deleted record succeeded (potential bug?)"); + + // Check if the record is still considered deleted + let get_result = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + ).await; + + if get_result.is_err() { + println!("Deleted Record Test: Record still appears deleted after update"); + } else { + println!("Deleted Record Test: Record appears to be undeleted after update"); + } + } + Err(e) => { + if let Some(status) = e.downcast_ref::() { + assert_eq!(status.code(), tonic::Code::NotFound, + "UPDATE on deleted record should return NotFound"); + println!("Deleted Record Test: UPDATE correctly rejected on deleted record"); + } + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_delete_already_deleted_record(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // 1. Create and delete a record + let initial_data = context.create_test_form_data(); + let post_result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + initial_data, + ).await; + + if let Ok(post_response) = post_result { + let record_id = post_response.inserted_id; + + // First deletion + let delete_result1 = context.client.delete_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + ).await; + assert!(delete_result1.is_ok(), "First delete should succeed"); + + // Second deletion (idempotent) + let delete_result2 = context.client.delete_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + ).await; + + assert!(delete_result2.is_ok(), "Second delete should succeed (idempotent)"); + if let Ok(response) = delete_result2 { + assert!(response.success, "Delete should report success even for already-deleted record"); + } + println!("Double Delete Test: Both deletions succeeded (idempotent behavior)"); + } +} + +// ======================================================================== +// VALIDATION AND BOUNDARY TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_large_data_handling(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test with very large string values + let large_string = "A".repeat(10000); // 10KB string + let very_large_string = "B".repeat(100000); // 100KB string + + let test_cases = vec![ + ("Large", large_string), + ("VeryLarge", very_large_string), + ]; + + for (case_name, large_value) in test_cases { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value(&large_value)); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + match result { + Ok(response) => { + println!("Large Data Test: {} string handled successfully", case_name); + + // Verify round-trip + if let Ok(get_response) = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + response.inserted_id, + ).await { + if let Some(retrieved_value) = get_response.data.get("firma") { + assert_eq!(retrieved_value.len(), large_value.len(), + "Large string should survive round-trip for case: {}", case_name); + } + } + } + Err(e) => { + println!("Large Data Test: {} failed (may hit size limits): {}", case_name, e); + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_sql_injection_attempts(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test potential SQL injection strings + let injection_attempts = vec![ + ("SingleQuote", "'; DROP TABLE users; --"), + ("DoubleQuote", "\"; DROP TABLE users; --"), + ("Union", "' UNION SELECT * FROM users --"), + ("Comment", "/* malicious comment */"), + ("Semicolon", "; DELETE FROM users;"), + ]; + + for (case_name, injection_string) in injection_attempts { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value(injection_string)); + data.insert("kz".to_string(), create_string_value("KZ123")); + + let result = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await; + + match result { + Ok(response) => { + println!("SQL Injection Test: {} handled safely (parameterized queries)", case_name); + + // Verify the malicious string was stored as-is (not executed) + if let Ok(get_response) = context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + response.inserted_id, + ).await { + if let Some(retrieved_value) = get_response.data.get("firma") { + assert_eq!(retrieved_value, injection_string, + "Injection string should be stored literally for case: {}", case_name); + } + } + } + Err(e) => { + println!("SQL Injection Test: {} rejected: {}", case_name, e); + } + } + } +} + +#[rstest] +#[tokio::test] +async fn test_concurrent_operations_with_same_data(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + // Test multiple concurrent operations with identical data + let mut handles = Vec::new(); + let num_tasks = 10; + + for i in 0..num_tasks { + let context_clone = context.clone(); + let handle = tokio::spawn(async move { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value("Concurrent Identical")); + data.insert("kz".to_string(), create_string_value(&format!("SAME{:02}", i))); + + context_clone.client.post_table_data( + context_clone.profile_name, + context_clone.table_name, + data, + ).await + }); + handles.push(handle); + } + + // Wait for all to complete + let mut success_count = 0; + let mut inserted_ids = Vec::new(); + + for (i, handle) in handles.into_iter().enumerate() { + match handle.await { + Ok(Ok(response)) => { + success_count += 1; + inserted_ids.push(response.inserted_id); + println!("Concurrent Identical Data: Task {} succeeded with ID {}", i, response.inserted_id); + } + Ok(Err(e)) => { + println!("Concurrent Identical Data: Task {} failed: {}", i, e); + } + Err(e) => { + println!("Concurrent Identical Data: Task {} panicked: {}", i, e); + } + } + } + + assert!(success_count > 0, "At least some concurrent operations should succeed"); + + // Verify all IDs are unique + let unique_ids: std::collections::HashSet<_> = inserted_ids.iter().collect(); + assert_eq!(unique_ids.len(), inserted_ids.len(), "All inserted IDs should be unique"); + + println!("Concurrent Identical Data: {}/{} operations succeeded with unique IDs", + success_count, num_tasks); +} + +// ======================================================================== +// PERFORMANCE AND STRESS TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_bulk_operations_performance(#[future] form_test_context: FormTestContext) { + let mut context = form_test_context.await; + skip_if_backend_unavailable!(); + + let operation_count = 50; + let start_time = std::time::Instant::now(); + + let mut successful_operations = 0; + let mut created_ids = Vec::new(); + + // Bulk create + for i in 0..operation_count { + let mut data = HashMap::new(); + data.insert("firma".to_string(), create_string_value(&format!("Bulk Company {}", i))); + data.insert("kz".to_string(), create_string_value(&format!("BLK{:02}", i))); + + if let Ok(response) = context.client.post_table_data( + context.profile_name.clone(), + context.table_name.clone(), + data, + ).await { + successful_operations += 1; + created_ids.push(response.inserted_id); + } + } + + let create_duration = start_time.elapsed(); + println!("Bulk Performance: Created {} records in {:?}", successful_operations, create_duration); + + // Bulk read + let read_start = std::time::Instant::now(); + let mut successful_reads = 0; + + for &record_id in &created_ids { + if context.client.get_table_data( + context.profile_name.clone(), + context.table_name.clone(), + record_id, + ).await.is_ok() { + successful_reads += 1; + } + } + + let read_duration = read_start.elapsed(); + println!("Bulk Performance: Read {} records in {:?}", successful_reads, read_duration); + + // Performance assertions + assert!(successful_operations > operation_count * 8 / 10, + "At least 80% of operations should succeed"); + assert!(create_duration.as_secs() < 60, + "Bulk operations should complete in reasonable time"); + + println!("Bulk Performance Test: {}/{} creates, {}/{} reads successful", + successful_operations, operation_count, successful_reads, created_ids.len()); +}