From d390c567d59c38ede5138c90c013fbf00330bf1a Mon Sep 17 00:00:00 2001 From: filipriec Date: Tue, 24 Jun 2025 00:46:51 +0200 Subject: [PATCH] more tests --- .../handlers/put_table_data_test3.rs | 513 ++++++++++++++++++ 1 file changed, 513 insertions(+) diff --git a/server/tests/tables_data/handlers/put_table_data_test3.rs b/server/tests/tables_data/handlers/put_table_data_test3.rs index 0e4e1d5..cfd470f 100644 --- a/server/tests/tables_data/handlers/put_table_data_test3.rs +++ b/server/tests/tables_data/handlers/put_table_data_test3.rs @@ -488,3 +488,516 @@ async fn test_update_nonexistent_foreign_key_reference( } } +#[rstest] +#[tokio::test] +async fn test_update_optional_foreign_key_to_null( + #[future] foreign_key_update_test_context: ForeignKeyUpdateTestContext, +) { + let context = foreign_key_update_test_context.await; + + // Create required entities + let mut category_data = HashMap::new(); + category_data.insert("name".to_string(), string_to_proto_value("Books")); + let category_request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.category_table.clone(), + data: category_data, + }; + let category_response = post_table_data(&context.pool, category_request, &context.indexer_tx).await.unwrap(); + + let mut product_data = HashMap::new(); + product_data.insert("name".to_string(), string_to_proto_value("Book")); + product_data.insert("price".to_string(), string_to_proto_value("29.99")); + product_data.insert(format!("{}_id", context.category_table), Value { kind: Some(Kind::NumberValue(category_response.inserted_id as f64)) }); + + let product_request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.product_table.clone(), + data: product_data, + }; + let product_response = post_table_data(&context.pool, product_request, &context.indexer_tx).await.unwrap(); + + // Create order with both required and optional foreign keys + let mut order_data = HashMap::new(); + order_data.insert("quantity".to_string(), Value { kind: Some(Kind::NumberValue(2.0)) }); + order_data.insert("notes".to_string(), string_to_proto_value("Initial order")); + order_data.insert(format!("{}_id", context.product_table), Value { kind: Some(Kind::NumberValue(product_response.inserted_id as f64)) }); + order_data.insert(format!("{}_id", context.category_table), Value { kind: Some(Kind::NumberValue(category_response.inserted_id as f64)) }); + + let order_request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.order_table.clone(), + data: order_data, + }; + let order_response = post_table_data(&context.pool, order_request, &context.indexer_tx).await.unwrap(); + + // Update to set optional foreign key to null + let mut update_data = HashMap::new(); + update_data.insert("notes".to_string(), string_to_proto_value("Updated order - no category")); + update_data.insert(format!("{}_id", context.category_table), Value { kind: Some(Kind::NullValue(0)) }); + + let update_request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.order_table.clone(), + id: order_response.inserted_id, + data: update_data, + }; + + let result = put_table_data(&context.pool, update_request, &context.indexer_tx).await; + assert!(result.is_ok(), "Update optional foreign key to null should succeed"); + + // Verify the optional foreign key is now null + let query = format!( + r#"SELECT notes, "{}_id" FROM "{}"."{}" WHERE id = $1"#, + context.category_table, context.profile_name, context.order_table + ); + let row = sqlx::query(&query) + .bind(order_response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let notes: String = row.get("notes"); + let category_id: Option = row.get(format!("{}_id", context.category_table).as_str()); + + assert_eq!(notes, "Updated order - no category"); + assert!(category_id.is_none()); +} + +// ======================================================================== +// ADVANCED DECIMAL UPDATE TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_update_decimal_precision_comprehensive( + #[future] financial_update_test_context: FinancialUpdateTestContext, +) { + let context = financial_update_test_context.await; + let record_id = create_initial_financial_update_record(&context).await; + + let decimal_test_cases = vec![ + ("price", "1599.9999", dec!(1599.9999)), + ("rate", "0.12345", dec!(0.12345)), + ("discount", "99.999", dec!(99.999)), + ("price", "-1234.5678", dec!(-1234.5678)), + ("rate", "-0.99999", dec!(-0.99999)), + ]; + + for (i, (field, value_str, expected_decimal)) in decimal_test_cases.into_iter().enumerate() { + let mut update_data = HashMap::new(); + update_data.insert("product_name".to_string(), string_to_proto_value(&format!("Precision Test {}", i))); + update_data.insert(field.to_string(), string_to_proto_value(value_str)); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.table_name.clone(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&context.pool, request, &context.indexer_tx) + .await + .unwrap(); + assert!(response.success); + + // Verify precision was preserved + let query = format!( + r#"SELECT {} FROM "{}"."{}" WHERE id = $1"#, + field, context.profile_name, context.table_name + ); + let stored_value: rust_decimal::Decimal = sqlx::query_scalar(&query) + .bind(record_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + assert_eq!(stored_value, expected_decimal, "Precision mismatch for field {}", field); + } +} + +#[rstest] +#[tokio::test] +async fn test_update_decimal_rounding_behavior( + #[future] financial_update_test_context: FinancialUpdateTestContext, +) { + let context = financial_update_test_context.await; + let record_id = create_initial_financial_update_record(&context).await; + + // Test values that require rounding based on column precision + let rounding_test_cases = vec![ + ("price", "99.123456", dec!(99.1235)), // decimal(19,4) rounds to 4 places + ("rate", "0.123456", dec!(0.12346)), // decimal(10,5) rounds to 5 places + ("discount", "12.3456", dec!(12.346)), // decimal(5,3) rounds to 3 places + ]; + + for (field, input_value, expected_rounded) in rounding_test_cases { + let mut update_data = HashMap::new(); + update_data.insert(field.to_string(), string_to_proto_value(input_value)); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.table_name.clone(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&context.pool, request, &context.indexer_tx) + .await + .unwrap(); + assert!(response.success); + + // Verify rounding was applied correctly + let query = format!( + r#"SELECT {} FROM "{}"."{}" WHERE id = $1"#, + field, context.profile_name, context.table_name + ); + let stored_value: rust_decimal::Decimal = sqlx::query_scalar(&query) + .bind(record_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + assert_eq!(stored_value, expected_rounded, "Rounding mismatch for field {}", field); + } +} + +#[rstest] +#[tokio::test] +async fn test_update_decimal_overflow_handling( + #[future] financial_update_test_context: FinancialUpdateTestContext, +) { + let context = financial_update_test_context.await; + let record_id = create_initial_financial_update_record(&context).await; + + // Test values that exceed precision limits + let overflow_test_cases = vec![ + ("discount", "1000.123"), // discount is decimal(5,3) - max is 99.999 + ("rate", "123456.12345"), // rate is decimal(10,5) - too many digits before decimal + ]; + + for (field, overflow_value) in overflow_test_cases { + let mut update_data = HashMap::new(); + update_data.insert(field.to_string(), string_to_proto_value(overflow_value)); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.table_name.clone(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&context.pool, request, &context.indexer_tx).await; + assert!(result.is_err(), "Should fail for decimal overflow: {} = {}", field, overflow_value); + + if let Err(err) = result { + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("Numeric field overflow")); + } + } +} + +// ======================================================================== +// INTEGER ROBUSTNESS UPDATE TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_update_integer_boundary_values_comprehensive( + #[future] integer_robustness_test_context: IntegerRobustnessTestContext, +) { + let context = integer_robustness_test_context.await; + + // Test i32 boundaries on INTEGER columns + let record_id = create_initial_integer_record(&context, &context.integer_only_table).await; + + let i32_boundary_tests = vec![ + (2147483647.0, "i32::MAX"), + (-2147483648.0, "i32::MIN"), + (2147483646.0, "i32::MAX - 1"), + (-2147483647.0, "i32::MIN + 1"), + (0.0, "zero"), + (1.0, "one"), + (-1.0, "negative one"), + ]; + + for (value, description) in i32_boundary_tests { + let mut update_data = HashMap::new(); + update_data.insert("name".to_string(), string_to_proto_value(&format!("i32 boundary test: {}", description))); + update_data.insert("value1".to_string(), Value { kind: Some(Kind::NumberValue(value)) }); + update_data.insert("value2".to_string(), Value { kind: Some(Kind::NumberValue(value)) }); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.integer_only_table.clone(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&context.pool, request, &context.indexer_tx).await; + assert!(result.is_ok(), "Failed for i32 value {}: {}", value, description); + + // Verify correct storage + let query = format!( + r#"SELECT value1, value2 FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.integer_only_table + ); + let row = sqlx::query(&query) + .bind(record_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_val1: i32 = row.get("value1"); + let stored_val2: i32 = row.get("value2"); + assert_eq!(stored_val1, value as i32); + assert_eq!(stored_val2, value as i32); + } +} + +#[rstest] +#[tokio::test] +async fn test_update_bigint_boundary_values_comprehensive( + #[future] integer_robustness_test_context: IntegerRobustnessTestContext, +) { + let context = integer_robustness_test_context.await; + let record_id = create_initial_integer_record(&context, &context.bigint_only_table).await; + + let i64_boundary_tests = vec![ + (9223372036854774784.0, "Close to i64::MAX (precisely representable)"), + (-9223372036854774784.0, "Close to i64::MIN (precisely representable)"), + (4611686018427387904.0, "i64::MAX / 2"), + (-4611686018427387904.0, "i64::MIN / 2"), + (1000000000000.0, "One trillion"), + (-1000000000000.0, "Negative one trillion"), + ]; + + for (value, description) in i64_boundary_tests { + let mut update_data = HashMap::new(); + update_data.insert("name".to_string(), string_to_proto_value(&format!("i64 test: {}", description))); + update_data.insert("value1".to_string(), Value { kind: Some(Kind::NumberValue(value)) }); + update_data.insert("value2".to_string(), Value { kind: Some(Kind::NumberValue(value)) }); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.bigint_only_table.clone(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&context.pool, request, &context.indexer_tx).await; + assert!(result.is_ok(), "Failed for i64 value {}: {}", value, description); + + // Verify correct storage + let query = format!( + r#"SELECT value1, value2 FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.bigint_only_table + ); + let row = sqlx::query(&query) + .bind(record_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_val1: i64 = row.get("value1"); + let stored_val2: i64 = row.get("value2"); + assert_eq!(stored_val1, value as i64); + assert_eq!(stored_val2, value as i64); + } +} + +#[rstest] +#[tokio::test] +async fn test_update_integer_overflow_rejection_i32( + #[future] integer_robustness_test_context: IntegerRobustnessTestContext, +) { + let context = integer_robustness_test_context.await; + let record_id = create_initial_integer_record(&context, &context.integer_only_table).await; + + let overflow_values = vec![ + (2147483648.0, "i32::MAX + 1"), + (-2147483649.0, "i32::MIN - 1"), + (3000000000.0, "3 billion"), + (-3000000000.0, "negative 3 billion"), + (4294967296.0, "2^32"), + ]; + + for (value, description) in overflow_values { + let mut update_data = HashMap::new(); + update_data.insert("name".to_string(), string_to_proto_value(&format!("Overflow test: {}", description))); + update_data.insert("value1".to_string(), Value { kind: Some(Kind::NumberValue(value)) }); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.integer_only_table.clone(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&context.pool, request, &context.indexer_tx).await; + assert!(result.is_err(), "Should have failed for i32 overflow value {}: {}", value, description); + + if let Err(err) = result { + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("Integer value out of range for INTEGER column")); + } + } +} + +#[rstest] +#[tokio::test] +async fn test_update_float_precision_edge_cases( + #[future] integer_robustness_test_context: IntegerRobustnessTestContext, +) { + let context = integer_robustness_test_context.await; + let record_id = create_initial_integer_record(&context, &context.integer_only_table).await; + + // Test values that have fractional parts (should be rejected) + let fractional_values = vec![ + (42.1, "42.1"), + (42.9, "42.9"), + (42.000001, "42.000001"), + (-42.5, "-42.5"), + (0.1, "0.1"), + (2147483646.5, "Near i32::MAX with fraction"), + ]; + + for (value, description) in fractional_values { + let mut update_data = HashMap::new(); + update_data.insert("name".to_string(), string_to_proto_value(&format!("Float test: {}", description))); + update_data.insert("value1".to_string(), Value { kind: Some(Kind::NumberValue(value)) }); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.integer_only_table.clone(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&context.pool, request, &context.indexer_tx).await; + assert!(result.is_err(), "Should fail for fractional value {}: {}", value, description); + + if let Err(err) = result { + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("Expected integer for column") && err.message().contains("but got a float")); + } + } +} + +#[rstest] +#[tokio::test] +async fn test_update_mixed_integer_types_same_record( + #[future] integer_robustness_test_context: IntegerRobustnessTestContext, +) { + let context = integer_robustness_test_context.await; + let record_id = create_initial_integer_record(&context, &context.mixed_integer_table).await; + + // Test updating different values into different integer types in the same record + let test_cases = vec![ + (42.0, 1000000000000.0, "Small i32, large i64"), + (2147483647.0, 9223372036854774784.0, "i32::MAX, near i64::MAX"), + (-2147483648.0, -9223372036854774784.0, "i32::MIN, near i64::MIN"), + (0.0, 0.0, "Both zero"), + (-1.0, -1.0, "Both negative one"), + ]; + + for (i32_val, i64_val, description) in test_cases { + let mut update_data = HashMap::new(); + update_data.insert("name".to_string(), string_to_proto_value(&format!("Mixed update test: {}", description))); + update_data.insert("small_int".to_string(), Value { kind: Some(Kind::NumberValue(i32_val)) }); + update_data.insert("big_int".to_string(), Value { kind: Some(Kind::NumberValue(i64_val)) }); + update_data.insert("another_int".to_string(), Value { kind: Some(Kind::NumberValue(i32_val)) }); + update_data.insert("another_bigint".to_string(), Value { kind: Some(Kind::NumberValue(i64_val)) }); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.mixed_integer_table.clone(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&context.pool, request, &context.indexer_tx).await; + assert!(result.is_ok(), "Failed for mixed integer update test: {}", description); + + // Verify correct storage with correct types + let query = format!( + r#"SELECT small_int, big_int, another_int, another_bigint FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.mixed_integer_table + ); + let row = sqlx::query(&query) + .bind(record_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_small_int: i32 = row.get("small_int"); + let stored_big_int: i64 = row.get("big_int"); + let stored_another_int: i32 = row.get("another_int"); + let stored_another_bigint: i64 = row.get("another_bigint"); + + assert_eq!(stored_small_int, i32_val as i32); + assert_eq!(stored_big_int, i64_val as i64); + assert_eq!(stored_another_int, i32_val as i32); + assert_eq!(stored_another_bigint, i64_val as i64); + } +} + +// ======================================================================== +// ADVANCED MIXED DATA TYPE UPDATE TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_update_mixed_null_and_valid_data_comprehensive( + #[future] advanced_data_type_test_context: AdvancedDataTypeTestContext, +) { + let context = advanced_data_type_test_context.await; + let record_id = create_initial_advanced_record(&context).await; + + // Update with mix of nulls and valid values + let mut update_data = HashMap::new(); + update_data.insert("my_text".to_string(), string_to_proto_value("Updated with mixed data")); + update_data.insert("my_bool".to_string(), bool_to_proto_value(true)); + update_data.insert("my_timestamp".to_string(), Value { kind: Some(Kind::NullValue(0)) }); + update_data.insert("my_bigint".to_string(), Value { kind: Some(Kind::NumberValue(42.0)) }); + update_data.insert("my_money".to_string(), Value { kind: Some(Kind::NullValue(0)) }); + update_data.insert("my_decimal".to_string(), string_to_proto_value("999.99")); + update_data.insert("my_real_bigint".to_string(), Value { kind: Some(Kind::NumberValue(9223372036854774784.0)) }); + + let request = PutTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.table_name.clone(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&context.pool, request, &context.indexer_tx).await.unwrap(); + assert!(response.success); + + // Verify mixed null and valid data was stored correctly + let query = format!( + r#"SELECT my_text, my_bool, my_timestamp, my_bigint, my_money, my_decimal, my_real_bigint FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + let row = sqlx::query(&query) + .bind(record_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_text: String = row.get("my_text"); + let stored_bool: bool = row.get("my_bool"); + let stored_timestamp: Option> = row.get("my_timestamp"); + let stored_bigint: i32 = row.get("my_bigint"); + let stored_money: Option = row.get("my_money"); + let stored_decimal: rust_decimal::Decimal = row.get("my_decimal"); + let stored_real_bigint: i64 = row.get("my_real_bigint"); + + assert_eq!(stored_text, "Updated with mixed data"); + assert_eq!(stored_bool, true); + assert!(stored_timestamp.is_none()); + assert_eq!(stored_bigint, 42); + assert!(stored_money.is_none()); + assert_eq!(stored_decimal, dec!(999.99)); + assert_eq!(stored_real_bigint, 9223372036854774784); +}