more put tests
This commit is contained in:
@@ -5,7 +5,7 @@ use sqlx::{PgPool, Row};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use prost_types::{value::Kind, Value};
|
use prost_types::{value::Kind, Value};
|
||||||
use common::proto::multieko2::table_definition::{
|
use common::proto::multieko2::table_definition::{
|
||||||
PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition,
|
PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition, TableLink,
|
||||||
};
|
};
|
||||||
use common::proto::multieko2::tables_data::{
|
use common::proto::multieko2::tables_data::{
|
||||||
PostTableDataRequest, PutTableDataRequest,
|
PostTableDataRequest, PutTableDataRequest,
|
||||||
@@ -20,11 +20,12 @@ use tokio::sync::mpsc;
|
|||||||
use server::indexer::IndexCommand;
|
use server::indexer::IndexCommand;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
|
use futures;
|
||||||
|
|
||||||
// ========= Test Helpers =========
|
// ========= Test Helpers =========
|
||||||
|
|
||||||
fn generate_unique_id() -> String {
|
fn generate_unique_id() -> String {
|
||||||
rand::thread_rng()
|
rand::rng()
|
||||||
.sample_iter(&Alphanumeric)
|
.sample_iter(&Alphanumeric)
|
||||||
.take(8)
|
.take(8)
|
||||||
.map(char::from)
|
.map(char::from)
|
||||||
@@ -66,11 +67,6 @@ async fn create_adresar_table(
|
|||||||
Ok(())
|
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
|
// Helper to create a record and return its ID for tests
|
||||||
async fn create_initial_record(
|
async fn create_initial_record(
|
||||||
context: &TestContext,
|
context: &TestContext,
|
||||||
@@ -538,3 +534,6 @@ async fn test_update_boolean_system_column_validation(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(deleted);
|
assert!(deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include!("put_table_data_test2.rs");
|
||||||
|
include!("put_table_data_test3.rs");
|
||||||
|
|||||||
833
server/tests/tables_data/handlers/put_table_data_test2.rs
Normal file
833
server/tests/tables_data/handlers/put_table_data_test2.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
490
server/tests/tables_data/handlers/put_table_data_test3.rs
Normal file
490
server/tests/tables_data/handlers/put_table_data_test3.rs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user