more frontend tests
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
// client/tests/form_request_tests.rs
|
// client/tests/form_request_tests.rs
|
||||||
use rstest::{fixture, rstest};
|
pub use rstest::{fixture, rstest};
|
||||||
use client::services::grpc_client::GrpcClient;
|
pub use client::services::grpc_client::GrpcClient;
|
||||||
use client::state::pages::form::FormState;
|
pub use client::state::pages::form::FormState;
|
||||||
use client::state::pages::canvas_state::CanvasState;
|
pub use client::state::pages::canvas_state::CanvasState;
|
||||||
use prost_types::Value;
|
pub use prost_types::Value;
|
||||||
use prost_types::value::Kind;
|
pub use prost_types::value::Kind;
|
||||||
use std::collections::HashMap;
|
pub use std::collections::HashMap;
|
||||||
use tonic::Status;
|
pub use tonic::Status;
|
||||||
use tokio::time::{timeout, Duration};
|
pub use tokio::time::{timeout, Duration};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
pub use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// HELPER FUNCTIONS AND UTILITIES
|
// 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_tests2.rs");
|
||||||
|
// include!("form_request_tests3.rs");
|
||||||
|
|||||||
728
client/tests/form/requests/form_request_tests3.rs
Normal file
728
client/tests/form/requests/form_request_tests3.rs
Normal file
@@ -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::<Status>() {
|
||||||
|
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::<Status>() {
|
||||||
|
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::<Status>() {
|
||||||
|
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::<Status>() {
|
||||||
|
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::<Status>() {
|
||||||
|
// 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::<Status>() {
|
||||||
|
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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user