testing frontend to connect to the backend in the form page

This commit is contained in:
filipriec
2025-06-26 19:19:08 +02:00
parent 5cd324b6ae
commit f6d84e70cc
4 changed files with 1096 additions and 827 deletions

1
Cargo.lock generated
View File

@@ -549,6 +549,7 @@ dependencies = [
"crossterm",
"dirs 6.0.0",
"dotenvy",
"futures",
"lazy_static",
"prost",
"prost-types",

View File

@@ -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"

View File

@@ -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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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::<Status>() {
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");

File diff suppressed because it is too large Load Diff