more put tests

This commit is contained in:
filipriec
2025-06-24 00:45:37 +02:00
parent f9a78e4eec
commit 029e614b9c
3 changed files with 1329 additions and 7 deletions

View File

@@ -5,7 +5,7 @@ use sqlx::{PgPool, Row};
use std::collections::HashMap;
use prost_types::{value::Kind, Value};
use common::proto::multieko2::table_definition::{
PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition,
PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition, TableLink,
};
use common::proto::multieko2::tables_data::{
PostTableDataRequest, PutTableDataRequest,
@@ -20,11 +20,12 @@ use tokio::sync::mpsc;
use server::indexer::IndexCommand;
use rand::Rng;
use rand::distr::Alphanumeric;
use futures;
// ========= Test Helpers =========
fn generate_unique_id() -> String {
rand::thread_rng()
rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
@@ -66,11 +67,6 @@ async fn create_adresar_table(
Ok(())
}
async fn create_test_indexer_channel(
) -> (mpsc::Sender<IndexCommand>, mpsc::Receiver<IndexCommand>) {
mpsc::channel(100)
}
// Helper to create a record and return its ID for tests
async fn create_initial_record(
context: &TestContext,
@@ -538,3 +534,6 @@ async fn test_update_boolean_system_column_validation(
.unwrap();
assert!(deleted);
}
include!("put_table_data_test2.rs");
include!("put_table_data_test3.rs");

View File

@@ -0,0 +1,833 @@
// tests/tables_data/handlers/put_table_data_test2.rs
// ========================================================================
// ADDITIONAL HELPER FUNCTIONS FOR COMPREHENSIVE PUT TESTS
// ========================================================================
// Additional imports needed for these tests
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use std::str::FromStr;
fn create_string_value(s: &str) -> Value {
Value { kind: Some(Kind::StringValue(s.to_string())) }
}
fn create_number_value(n: f64) -> Value {
Value { kind: Some(Kind::NumberValue(n)) }
}
fn create_bool_value(b: bool) -> Value {
Value { kind: Some(Kind::BoolValue(b)) }
}
fn create_null_value() -> Value {
Value { kind: Some(Kind::NullValue(0)) }
}
// Helper to create a table with various data types for comprehensive testing
async fn create_data_type_test_table(
pool: &PgPool,
table_name: &str,
profile_name: &str
) -> Result<(), tonic::Status> {
let table_def_request = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: table_name.into(),
columns: vec![
TableColumnDefinition { name: "my_text".into(), field_type: "text".into() },
TableColumnDefinition { name: "my_bool".into(), field_type: "boolean".into() },
TableColumnDefinition { name: "my_timestamp".into(), field_type: "timestamptz".into() },
TableColumnDefinition { name: "my_bigint".into(), field_type: "integer".into() },
TableColumnDefinition { name: "my_money".into(), field_type: "decimal(19,4)".into() },
TableColumnDefinition { name: "my_decimal".into(), field_type: "decimal(10,2)".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, table_def_request).await?;
Ok(())
}
// Helper to create financial table for decimal tests
async fn create_financial_table(
pool: &PgPool,
table_name: &str,
profile_name: &str,
) -> Result<(), tonic::Status> {
let table_def_request = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: table_name.into(),
columns: vec![
TableColumnDefinition { name: "product_name".into(), field_type: "text".into() },
TableColumnDefinition { name: "price".into(), field_type: "decimal(19, 4)".into() },
TableColumnDefinition { name: "rate".into(), field_type: "decimal(10, 5)".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, table_def_request).await?;
Ok(())
}
// ========================================================================
// TEST CONTEXTS FOR DIFFERENT TABLE TYPES
// ========================================================================
#[derive(Clone)]
struct DataTypeTestContext {
pool: PgPool,
profile_name: String,
table_name: String,
indexer_tx: mpsc::Sender<IndexCommand>,
}
#[derive(Clone)]
struct FinancialTestContext {
pool: PgPool,
profile_name: String,
table_name: String,
indexer_tx: mpsc::Sender<IndexCommand>,
}
#[fixture]
async fn data_type_test_context() -> DataTypeTestContext {
let pool = setup_test_db().await;
let unique_id = generate_unique_id();
let profile_name = format!("dtype_profile_{}", unique_id);
let table_name = format!("dtype_table_{}", unique_id);
create_data_type_test_table(&pool, &table_name, &profile_name).await
.expect("Failed to create data type test table");
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move { while rx.recv().await.is_some() {} });
DataTypeTestContext { pool, profile_name, table_name, indexer_tx: tx }
}
#[fixture]
async fn financial_test_context() -> FinancialTestContext {
let pool = setup_test_db().await;
let unique_id = generate_unique_id();
let profile_name = format!("financial_profile_{}", unique_id);
let table_name = format!("invoices_{}", unique_id);
create_financial_table(&pool, &table_name, &profile_name).await
.expect("Failed to create financial test table");
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move { while rx.recv().await.is_some() {} });
FinancialTestContext { pool, profile_name, table_name, indexer_tx: tx }
}
// Helper to create initial record for data type tests
async fn create_initial_data_type_record(context: &DataTypeTestContext) -> i64 {
let mut initial_data = HashMap::new();
initial_data.insert("my_text".to_string(), string_to_proto_value("Initial Text"));
initial_data.insert("my_bool".to_string(), bool_to_proto_value(false));
initial_data.insert("my_timestamp".to_string(), string_to_proto_value("2024-01-01T12:00:00Z"));
initial_data.insert("my_bigint".to_string(), Value { kind: Some(Kind::NumberValue(100.0)) });
initial_data.insert("my_money".to_string(), string_to_proto_value("100.0000"));
initial_data.insert("my_decimal".to_string(), string_to_proto_value("50.00"));
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
data: initial_data,
};
let response = post_table_data(&context.pool, request, &context.indexer_tx)
.await
.expect("Setup: Failed to create initial record");
response.inserted_id
}
// Helper to create initial record for financial tests
async fn create_initial_financial_record(context: &FinancialTestContext) -> i64 {
let mut initial_data = HashMap::new();
initial_data.insert("product_name".to_string(), string_to_proto_value("Initial Product"));
initial_data.insert("price".to_string(), string_to_proto_value("100.0000"));
initial_data.insert("rate".to_string(), string_to_proto_value("1.00000"));
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
data: initial_data,
};
let response = post_table_data(&context.pool, request, &context.indexer_tx)
.await
.expect("Setup: Failed to create initial record");
response.inserted_id
}
// ========================================================================
// UNICODE AND SPECIAL CHARACTER TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_unicode_special_characters_comprehensive(
#[future] test_context: TestContext,
) {
let context = test_context.await;
let special_strings = vec![
"José María González", // Accented characters
"Москва", // Cyrillic
"北京市", // Chinese
"🚀 Tech Company 🌟", // Emoji
"Line\nBreak\tTab", // Control characters
"Quote\"Test'Apostrophe", // Quotes
"SQL'; DROP TABLE test; --", // SQL injection attempt
"Price: $1,000.50 (50% off!)", // Special symbols
];
for (i, test_string) in special_strings.into_iter().enumerate() {
let record_id = create_initial_record(
&context,
HashMap::from([("firma".to_string(), string_to_proto_value("Original"))]),
).await;
let mut update_data = HashMap::new();
update_data.insert("firma".to_string(), string_to_proto_value(test_string));
update_data.insert("kz".to_string(), string_to_proto_value(&format!("TEST{}", i)));
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, "Failed for string: '{}'", test_string);
// Verify the data was updated correctly
let query = format!(
r#"SELECT firma FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.table_name
);
let stored_firma: String = sqlx::query_scalar(&query)
.bind(record_id)
.fetch_one(&context.pool)
.await
.unwrap();
assert_eq!(stored_firma, test_string.trim());
}
}
#[rstest]
#[tokio::test]
async fn test_update_field_length_boundaries(#[future] test_context: TestContext) {
let context = test_context.await;
let length_test_cases = vec![
("1234567890123456", true), // 16 chars - should fail
("123456789012345", false), // 15 chars - should pass
("", false), // Empty - should pass (becomes NULL)
("1", false), // Single char - should pass
];
for (test_string, should_fail) in length_test_cases {
let record_id = create_initial_record(
&context,
HashMap::from([("telefon".to_string(), string_to_proto_value("555-1234"))]),
).await;
let mut update_data = HashMap::new();
update_data.insert("firma".to_string(), string_to_proto_value("Length Test Company"));
update_data.insert("telefon".to_string(), string_to_proto_value(test_string));
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;
if should_fail {
assert!(result.is_err(), "Should fail for telefon length: {}", test_string.len());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::Internal);
assert!(err.message().contains("Value too long for telefon"));
}
} else {
assert!(result.is_ok(), "Should succeed for telefon length: {}", test_string.len());
}
}
}
// ========================================================================
// LARGE TEXT FIELD TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_large_text_fields(#[future] test_context: TestContext) {
let context = test_context.await;
let sizes = vec![1000, 5000, 10000];
for size in sizes {
let large_text = "A".repeat(size);
let record_id = create_initial_record(
&context,
HashMap::from([("firma".to_string(), string_to_proto_value("Original"))]),
).await;
let mut update_data = HashMap::new();
update_data.insert("firma".to_string(), string_to_proto_value(&large_text));
update_data.insert("ulica".to_string(), string_to_proto_value(&format!("Street with {} chars", size)));
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, "Failed for size: {}", size);
// Verify the large text was stored correctly
let query = format!(
r#"SELECT firma FROM "{}"."{}" WHERE id = $1"#,
context.profile_name, context.table_name
);
let stored_firma: String = sqlx::query_scalar(&query)
.bind(record_id)
.fetch_one(&context.pool)
.await
.unwrap();
assert_eq!(stored_firma.len(), size);
assert_eq!(stored_firma, large_text);
}
}
// ========================================================================
// DATA TYPE VALIDATION TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_correct_data_types_success(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value("Updated String"));
data.insert("my_bool".into(), create_bool_value(true));
data.insert("my_timestamp".into(), create_string_value("2024-06-15T15:30:00Z"));
data.insert("my_bigint".into(), create_number_value(142.0));
data.insert("my_money".into(), create_string_value("223.45"));
data.insert("my_decimal".into(), create_string_value("899.99"));
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let response = put_table_data(&context.pool, request, &context.indexer_tx)
.await
.unwrap();
assert!(response.success);
assert_eq!(response.updated_id, record_id);
// Verify data was updated correctly
let query = format!(
r#"SELECT my_text, my_bool, my_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_bigint: i32 = row.get("my_bigint");
assert_eq!(stored_text, "Updated String");
assert_eq!(stored_bool, true);
assert_eq!(stored_bigint, 142);
}
#[rstest]
#[tokio::test]
async fn test_update_type_mismatch_string_for_boolean(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value("Updated field"));
data.insert("my_bool".into(), create_string_value("true")); // String instead of boolean
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert!(err.message().contains("Expected boolean for column 'my_bool'"));
}
}
#[rstest]
#[tokio::test]
async fn test_update_invalid_timestamp_format(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value("Updated field"));
data.insert("my_timestamp".into(), create_string_value("not-a-date"));
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert!(err.message().contains("Invalid timestamp for my_timestamp"));
}
}
#[rstest]
#[tokio::test]
async fn test_update_float_for_integer_field(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value("Updated field"));
data.insert("my_bigint".into(), create_number_value(123.45)); // Float for integer field
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert!(err.message().contains("Expected integer for column 'my_bigint', but got a float"));
}
}
// ========================================================================
// DECIMAL/NUMERIC DATA TYPE TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_valid_decimal_string(
#[future] financial_test_context: FinancialTestContext,
) {
let context = financial_test_context.await;
let record_id = create_initial_financial_record(&context).await;
let mut data = HashMap::new();
data.insert("product_name".into(), create_string_value("Updated Laptop"));
data.insert("price".into(), create_string_value("1599.99"));
data.insert("rate".into(), create_string_value("-0.54321"));
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let response = put_table_data(&context.pool, request, &context.indexer_tx)
.await
.unwrap();
assert!(response.success);
let query = format!(
r#"SELECT price, rate 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 price: Decimal = row.get("price");
let rate: Decimal = row.get("rate");
assert_eq!(price, Decimal::from_str("1599.99").unwrap());
assert_eq!(rate, Decimal::from_str("-0.54321").unwrap());
}
#[rstest]
#[tokio::test]
async fn test_update_decimal_from_number_fails(
#[future] financial_test_context: FinancialTestContext,
) {
let context = financial_test_context.await;
let record_id = create_initial_financial_record(&context).await;
let mut data = HashMap::new();
data.insert("product_name".into(), create_string_value("Updated Mouse"));
data.insert("price".into(), create_number_value(85.50)); // Number instead of string
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert!(status.message().contains("Expected a string representation for decimal column 'price'"));
}
#[rstest]
#[tokio::test]
async fn test_update_invalid_decimal_string_fails(
#[future] financial_test_context: FinancialTestContext,
) {
let context = financial_test_context.await;
let record_id = create_initial_financial_record(&context).await;
let mut data = HashMap::new();
data.insert("product_name".into(), create_string_value("Bad Data Update"));
data.insert("price".into(), create_string_value("not-a-number"));
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert!(status.message().contains("Invalid decimal string format for column 'price'"));
}
// ========================================================================
// INTEGER BOUNDARY TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_integer_boundary_values(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let boundary_values = vec![
(0.0, "zero"),
(1.0, "one"),
(-1.0, "negative one"),
(2147483647.0, "i32::MAX"),
(-2147483648.0, "i32::MIN"),
(2147483646.0, "i32::MAX - 1"),
(-2147483647.0, "i32::MIN + 1"),
];
for (value, description) in boundary_values {
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value(&format!("Boundary test: {}", description)));
data.insert("my_bigint".into(), create_number_value(value));
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_ok(), "Failed for boundary value: {} ({})", value, description);
}
}
#[rstest]
#[tokio::test]
async fn test_update_integer_overflow_rejection(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let overflow_values = vec![
(2147483648.0, "i32::MAX + 1"),
(-2147483649.0, "i32::MIN - 1"),
(3000000000.0, "3 billion"),
(-3000000000.0, "negative 3 billion"),
];
for (value, description) in overflow_values {
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value(&format!("Overflow test: {}", description)));
data.insert("my_bigint".into(), create_number_value(value));
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_err(), "Should have failed for 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"));
}
}
}
// ========================================================================
// PERFORMANCE AND STRESS TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_rapid_sequential_operations(#[future] test_context: TestContext) {
let context = test_context.await;
let start_time = std::time::Instant::now();
// Create initial records
let mut record_ids = Vec::new();
for i in 0..50 {
let record_id = create_initial_record(
&context,
HashMap::from([("firma".to_string(), string_to_proto_value(&format!("Original Company {}", i)))]),
).await;
record_ids.push(record_id);
}
// Perform rapid sequential updates
for (i, record_id) in record_ids.iter().enumerate() {
let mut update_data = HashMap::new();
update_data.insert("firma".to_string(), string_to_proto_value(&format!("Updated Rapid Company {}", i)));
update_data.insert("kz".to_string(), string_to_proto_value(&format!("RAP{}", i)));
update_data.insert("telefon".to_string(), string_to_proto_value(&format!("+421{:09}", i)));
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, "Rapid update {} should succeed", i);
}
let duration = start_time.elapsed();
println!("50 rapid updates took: {:?}", duration);
// Verify all records were updated
let query = format!(
r#"SELECT COUNT(*) FROM "{}"."{}" WHERE firma LIKE 'Updated Rapid Company%'"#,
context.profile_name, context.table_name
);
let count: i64 = sqlx::query_scalar(&query)
.fetch_one(&context.pool)
.await
.unwrap();
assert_eq!(count, 50);
}
// ========================================================================
// ERROR SCENARIO TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_nonexistent_profile_error(#[future] test_context: TestContext) {
let context = test_context.await;
let record_id = create_initial_record(
&context,
HashMap::from([("firma".to_string(), string_to_proto_value("Test Company"))]),
).await;
let mut update_data = HashMap::new();
update_data.insert("firma".to_string(), string_to_proto_value("Updated Company"));
let invalid_request = PutTableDataRequest {
profile_name: "nonexistent_profile".into(),
table_name: context.table_name.clone(),
id: record_id,
data: update_data,
};
let result = put_table_data(&context.pool, invalid_request, &context.indexer_tx).await;
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::NotFound);
assert!(err.message().contains("Profile not found"));
}
}
#[rstest]
#[tokio::test]
async fn test_update_nonexistent_table_error(#[future] test_context: TestContext) {
let context = test_context.await;
let record_id = create_initial_record(
&context,
HashMap::from([("firma".to_string(), string_to_proto_value("Test Company"))]),
).await;
let mut update_data = HashMap::new();
update_data.insert("firma".to_string(), string_to_proto_value("Updated Company"));
let invalid_request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: "nonexistent_table".into(),
id: record_id,
data: update_data,
};
let result = put_table_data(&context.pool, invalid_request, &context.indexer_tx).await;
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::NotFound);
assert!(err.message().contains("Table not found"));
}
}
// ========================================================================
// NULL VALUE HANDLING TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_null_values_for_all_types(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value("Updated but keep nulls"));
data.insert("my_bool".into(), create_null_value());
data.insert("my_timestamp".into(), create_null_value());
data.insert("my_bigint".into(), create_null_value());
data.insert("my_money".into(), create_null_value());
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let response = put_table_data(&context.pool, request, &context.indexer_tx)
.await
.unwrap();
assert!(response.success);
// Verify nulls were stored correctly
let query = format!(
r#"SELECT my_bool, my_timestamp, my_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_bool: Option<bool> = row.get("my_bool");
let stored_timestamp: Option<chrono::DateTime<Utc>> = row.get("my_timestamp");
let stored_bigint: Option<i32> = row.get("my_bigint");
assert!(stored_bool.is_none());
assert!(stored_timestamp.is_none());
assert!(stored_bigint.is_none());
}
// ========================================================================
// VALID TIMESTAMP FORMAT TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_valid_timestamp_formats(
#[future] data_type_test_context: DataTypeTestContext,
) {
let context = data_type_test_context.await;
let valid_timestamps = vec![
"2024-01-15T10:30:00Z",
"2024-01-15T10:30:00+00:00",
"2024-01-15T10:30:00.123Z",
"2024-12-31T23:59:59Z",
"1970-01-01T00:00:00Z", // Unix epoch
];
for (i, timestamp) in valid_timestamps.into_iter().enumerate() {
let record_id = create_initial_data_type_record(&context).await;
let mut data = HashMap::new();
data.insert("my_text".into(), create_string_value(&format!("Timestamp test {}", i)));
data.insert("my_timestamp".into(), create_string_value(timestamp));
let request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
id: record_id,
data,
};
let result = put_table_data(&context.pool, request, &context.indexer_tx).await;
assert!(result.is_ok(), "Failed for timestamp: {}", timestamp);
}
}

View File

@@ -0,0 +1,490 @@
// tests/tables_data/handlers/put_table_data_test3.rs
// ========================================================================
// ADDITIONAL HELPER FUNCTIONS FOR COMPREHENSIVE PUT TEST 3
// ========================================================================
use rust_decimal_macros::dec;
// Note: Helper functions like create_string_value, create_number_value, etc.
// are already defined in the main test file, so we don't redefine them here.
// ========================================================================
// CONTEXTS FOR ADVANCED TESTING
// ========================================================================
#[derive(Clone)]
struct AdvancedDataTypeTestContext {
pool: PgPool,
profile_name: String,
table_name: String,
indexer_tx: mpsc::Sender<IndexCommand>,
}
#[derive(Clone)]
struct ForeignKeyUpdateTestContext {
pool: PgPool,
profile_name: String,
category_table: String,
product_table: String,
order_table: String,
indexer_tx: mpsc::Sender<IndexCommand>,
}
#[derive(Clone)]
struct IntegerRobustnessTestContext {
pool: PgPool,
profile_name: String,
mixed_integer_table: String,
bigint_only_table: String,
integer_only_table: String,
indexer_tx: mpsc::Sender<IndexCommand>,
}
#[derive(Clone)]
struct FinancialUpdateTestContext {
pool: PgPool,
profile_name: String,
table_name: String,
indexer_tx: mpsc::Sender<IndexCommand>,
}
// ========================================================================
// TABLE CREATION HELPERS
// ========================================================================
async fn create_advanced_data_type_table(
pool: &PgPool,
table_name: &str,
profile_name: &str
) -> Result<(), tonic::Status> {
let table_def_request = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: table_name.into(),
columns: vec![
TableColumnDefinition { name: "my_text".into(), field_type: "text".into() },
TableColumnDefinition { name: "my_bool".into(), field_type: "boolean".into() },
TableColumnDefinition { name: "my_timestamp".into(), field_type: "timestamptz".into() },
TableColumnDefinition { name: "my_bigint".into(), field_type: "integer".into() },
TableColumnDefinition { name: "my_money".into(), field_type: "decimal(19,4)".into() },
TableColumnDefinition { name: "my_date".into(), field_type: "date".into() },
TableColumnDefinition { name: "my_decimal".into(), field_type: "decimal(10,2)".into() },
TableColumnDefinition { name: "my_real_bigint".into(), field_type: "biginteger".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, table_def_request).await?;
Ok(())
}
async fn create_foreign_key_test_tables(
pool: &PgPool,
profile_name: &str,
category_table: &str,
product_table: &str,
order_table: &str
) -> Result<(), tonic::Status> {
// Create category table first (no dependencies)
let category_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: category_table.into(),
columns: vec![
TableColumnDefinition { name: "name".into(), field_type: "text".into() },
TableColumnDefinition { name: "description".into(), field_type: "text".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, category_def).await?;
// Create product table with required link to category
let product_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: product_table.into(),
columns: vec![
TableColumnDefinition { name: "name".into(), field_type: "text".into() },
TableColumnDefinition { name: "price".into(), field_type: "decimal(10,2)".into() },
],
indexes: vec![],
links: vec![
TableLink { linked_table_name: category_table.into(), required: true },
],
};
post_table_definition(pool, product_def).await?;
// Create order table with required link to product and optional link to category
let order_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: order_table.into(),
columns: vec![
TableColumnDefinition { name: "quantity".into(), field_type: "integer".into() },
TableColumnDefinition { name: "notes".into(), field_type: "text".into() },
],
indexes: vec![],
links: vec![
TableLink { linked_table_name: product_table.into(), required: true },
TableLink { linked_table_name: category_table.into(), required: false },
],
};
post_table_definition(pool, order_def).await?;
Ok(())
}
async fn create_integer_robustness_tables(
pool: &PgPool,
profile_name: &str,
mixed_table: &str,
bigint_table: &str,
integer_table: &str
) -> Result<(), tonic::Status> {
// Table with both INTEGER and BIGINT columns
let mixed_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: mixed_table.into(),
columns: vec![
TableColumnDefinition { name: "name".into(), field_type: "text".into() },
TableColumnDefinition { name: "small_int".into(), field_type: "integer".into() },
TableColumnDefinition { name: "big_int".into(), field_type: "biginteger".into() },
TableColumnDefinition { name: "another_int".into(), field_type: "int".into() },
TableColumnDefinition { name: "another_bigint".into(), field_type: "bigint".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, mixed_def).await?;
// Table with only BIGINT columns
let bigint_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: bigint_table.into(),
columns: vec![
TableColumnDefinition { name: "name".into(), field_type: "text".into() },
TableColumnDefinition { name: "value1".into(), field_type: "biginteger".into() },
TableColumnDefinition { name: "value2".into(), field_type: "bigint".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, bigint_def).await?;
// Table with only INTEGER columns
let integer_def = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: integer_table.into(),
columns: vec![
TableColumnDefinition { name: "name".into(), field_type: "text".into() },
TableColumnDefinition { name: "value1".into(), field_type: "integer".into() },
TableColumnDefinition { name: "value2".into(), field_type: "int".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, integer_def).await?;
Ok(())
}
async fn create_financial_update_table(
pool: &PgPool,
table_name: &str,
profile_name: &str,
) -> Result<(), tonic::Status> {
let table_def_request = PostTableDefinitionRequest {
profile_name: profile_name.into(),
table_name: table_name.into(),
columns: vec![
TableColumnDefinition { name: "product_name".into(), field_type: "text".into() },
TableColumnDefinition { name: "price".into(), field_type: "decimal(19, 4)".into() },
TableColumnDefinition { name: "rate".into(), field_type: "decimal(10, 5)".into() },
TableColumnDefinition { name: "discount".into(), field_type: "decimal(5, 3)".into() },
],
indexes: vec![],
links: vec![],
};
post_table_definition(pool, table_def_request).await?;
Ok(())
}
// ========================================================================
// FIXTURES
// ========================================================================
#[fixture]
async fn advanced_data_type_test_context() -> AdvancedDataTypeTestContext {
let pool = setup_test_db().await;
let unique_id = generate_unique_id();
let profile_name = format!("adv_dtype_profile_{}", unique_id);
let table_name = format!("adv_dtype_table_{}", unique_id);
create_advanced_data_type_table(&pool, &table_name, &profile_name).await
.expect("Failed to create advanced data type test table");
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move { while rx.recv().await.is_some() {} });
AdvancedDataTypeTestContext { pool, profile_name, table_name, indexer_tx: tx }
}
#[fixture]
async fn foreign_key_update_test_context() -> ForeignKeyUpdateTestContext {
let pool = setup_test_db().await;
let unique_id = generate_unique_id();
let profile_name = format!("fk_update_profile_{}", unique_id);
let category_table = format!("category_upd_{}", unique_id);
let product_table = format!("product_upd_{}", unique_id);
let order_table = format!("order_upd_{}", unique_id);
create_foreign_key_test_tables(&pool, &profile_name, &category_table, &product_table, &order_table).await
.expect("Failed to create foreign key test tables");
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move { while rx.recv().await.is_some() {} });
ForeignKeyUpdateTestContext {
pool, profile_name, category_table, product_table, order_table, indexer_tx: tx
}
}
#[fixture]
async fn integer_robustness_test_context() -> IntegerRobustnessTestContext {
let pool = setup_test_db().await;
let unique_id = generate_unique_id();
let profile_name = format!("int_robust_profile_{}", unique_id);
let mixed_table = format!("mixed_int_table_{}", unique_id);
let bigint_table = format!("bigint_table_{}", unique_id);
let integer_table = format!("integer_table_{}", unique_id);
create_integer_robustness_tables(&pool, &profile_name, &mixed_table, &bigint_table, &integer_table).await
.expect("Failed to create integer robustness test tables");
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move { while rx.recv().await.is_some() {} });
IntegerRobustnessTestContext {
pool, profile_name, mixed_integer_table: mixed_table,
bigint_only_table: bigint_table, integer_only_table: integer_table, indexer_tx: tx
}
}
#[fixture]
async fn financial_update_test_context() -> FinancialUpdateTestContext {
let pool = setup_test_db().await;
let unique_id = generate_unique_id();
let profile_name = format!("financial_upd_profile_{}", unique_id);
let table_name = format!("invoices_upd_{}", unique_id);
create_financial_update_table(&pool, &table_name, &profile_name).await
.expect("Failed to create financial update test table");
let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move { while rx.recv().await.is_some() {} });
FinancialUpdateTestContext { pool, profile_name, table_name, indexer_tx: tx }
}
// ========================================================================
// HELPER FUNCTIONS FOR CREATING INITIAL RECORDS
// ========================================================================
async fn create_initial_advanced_record(context: &AdvancedDataTypeTestContext) -> i64 {
let mut initial_data = HashMap::new();
initial_data.insert("my_text".to_string(), string_to_proto_value("Initial Text"));
initial_data.insert("my_bool".to_string(), bool_to_proto_value(false));
initial_data.insert("my_timestamp".to_string(), string_to_proto_value("2024-01-01T12:00:00Z"));
initial_data.insert("my_bigint".to_string(), Value { kind: Some(Kind::NumberValue(100.0)) });
initial_data.insert("my_money".to_string(), string_to_proto_value("100.0000"));
initial_data.insert("my_decimal".to_string(), string_to_proto_value("50.00"));
initial_data.insert("my_real_bigint".to_string(), Value { kind: Some(Kind::NumberValue(1000000000000.0)) });
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
data: initial_data,
};
let response = post_table_data(&context.pool, request, &context.indexer_tx)
.await
.expect("Setup: Failed to create initial record");
response.inserted_id
}
// Fixed function name to avoid conflict - this version is for FinancialUpdateTestContext
async fn create_initial_financial_update_record(context: &FinancialUpdateTestContext) -> i64 {
let mut initial_data = HashMap::new();
initial_data.insert("product_name".to_string(), string_to_proto_value("Initial Product"));
initial_data.insert("price".to_string(), string_to_proto_value("100.0000"));
initial_data.insert("rate".to_string(), string_to_proto_value("1.00000"));
initial_data.insert("discount".to_string(), string_to_proto_value("0.100"));
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.table_name.clone(),
data: initial_data,
};
let response = post_table_data(&context.pool, request, &context.indexer_tx)
.await
.expect("Setup: Failed to create initial financial record");
response.inserted_id
}
async fn create_initial_integer_record(context: &IntegerRobustnessTestContext, table_name: &str) -> i64 {
let mut initial_data = HashMap::new();
initial_data.insert("name".to_string(), string_to_proto_value("Initial Record"));
match table_name {
table if table.contains("mixed") => {
initial_data.insert("small_int".to_string(), Value { kind: Some(Kind::NumberValue(100.0)) });
initial_data.insert("big_int".to_string(), Value { kind: Some(Kind::NumberValue(1000000000000.0)) });
initial_data.insert("another_int".to_string(), Value { kind: Some(Kind::NumberValue(200.0)) });
initial_data.insert("another_bigint".to_string(), Value { kind: Some(Kind::NumberValue(2000000000000.0)) });
},
table if table.contains("bigint") => {
initial_data.insert("value1".to_string(), Value { kind: Some(Kind::NumberValue(1000000000000.0)) });
initial_data.insert("value2".to_string(), Value { kind: Some(Kind::NumberValue(2000000000000.0)) });
},
table if table.contains("integer") => {
initial_data.insert("value1".to_string(), Value { kind: Some(Kind::NumberValue(100.0)) });
initial_data.insert("value2".to_string(), Value { kind: Some(Kind::NumberValue(200.0)) });
},
_ => {}
}
let request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: table_name.to_string(),
data: initial_data,
};
let response = post_table_data(&context.pool, request, &context.indexer_tx)
.await
.expect("Setup: Failed to create initial integer record");
response.inserted_id
}
// ========================================================================
// FOREIGN KEY UPDATE TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_valid_foreign_key_reference(
#[future] foreign_key_update_test_context: ForeignKeyUpdateTestContext,
) {
let context = foreign_key_update_test_context.await;
// Create categories
let mut category1_data = HashMap::new();
category1_data.insert("name".to_string(), string_to_proto_value("Electronics"));
let category1_request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.category_table.clone(),
data: category1_data,
};
let category1_response = post_table_data(&context.pool, category1_request, &context.indexer_tx).await.unwrap();
let mut category2_data = HashMap::new();
category2_data.insert("name".to_string(), string_to_proto_value("Books"));
let category2_request = PostTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.category_table.clone(),
data: category2_data,
};
let category2_response = post_table_data(&context.pool, category2_request, &context.indexer_tx).await.unwrap();
// Create product with category1
let mut product_data = HashMap::new();
product_data.insert("name".to_string(), string_to_proto_value("Laptop"));
product_data.insert("price".to_string(), string_to_proto_value("999.99"));
product_data.insert(format!("{}_id", context.category_table), Value { kind: Some(Kind::NumberValue(category1_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();
// Update product to reference category2
let mut update_data = HashMap::new();
update_data.insert("name".to_string(), string_to_proto_value("Programming Book"));
update_data.insert(format!("{}_id", context.category_table), Value { kind: Some(Kind::NumberValue(category2_response.inserted_id as f64)) });
let update_request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.product_table.clone(),
id: product_response.inserted_id,
data: update_data,
};
let result = put_table_data(&context.pool, update_request, &context.indexer_tx).await;
assert!(result.is_ok(), "Update with valid foreign key should succeed");
// Verify the foreign key was updated
let query = format!(
r#"SELECT name, "{}_id" FROM "{}"."{}" WHERE id = $1"#,
context.category_table, context.profile_name, context.product_table
);
let row = sqlx::query(&query)
.bind(product_response.inserted_id)
.fetch_one(&context.pool)
.await
.unwrap();
let name: String = row.get("name");
let category_id: i64 = row.get(format!("{}_id", context.category_table).as_str());
assert_eq!(name, "Programming Book");
assert_eq!(category_id, category2_response.inserted_id);
}
#[rstest]
#[tokio::test]
async fn test_update_nonexistent_foreign_key_reference(
#[future] foreign_key_update_test_context: ForeignKeyUpdateTestContext,
) {
let context = foreign_key_update_test_context.await;
// Create category and product
let mut category_data = HashMap::new();
category_data.insert("name".to_string(), string_to_proto_value("Electronics"));
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("Laptop"));
product_data.insert("price".to_string(), string_to_proto_value("999.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();
// Try to update product to reference non-existent category
let mut update_data = HashMap::new();
update_data.insert(format!("{}_id", context.category_table), Value { kind: Some(Kind::NumberValue(99999.0)) });
let update_request = PutTableDataRequest {
profile_name: context.profile_name.clone(),
table_name: context.product_table.clone(),
id: product_response.inserted_id,
data: update_data,
};
let result = put_table_data(&context.pool, update_request, &context.indexer_tx).await;
assert!(result.is_err(), "Update with non-existent foreign key should fail");
if let Err(err) = result {
assert_eq!(err.code(), tonic::Code::Internal);
assert!(err.message().contains("Update failed"));
}
}