more tests

This commit is contained in:
filipriec
2025-06-24 00:46:51 +02:00
parent 029e614b9c
commit d390c567d5

View File

@@ -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<i64> = 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<chrono::DateTime<chrono::Utc>> = row.get("my_timestamp");
let stored_bigint: i32 = row.get("my_bigint");
let stored_money: Option<rust_decimal::Decimal> = 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);
}