diff --git a/server/tests/tables_data/mod.rs b/server/tests/tables_data/mod.rs index ae15c80..7621836 100644 --- a/server/tests/tables_data/mod.rs +++ b/server/tests/tables_data/mod.rs @@ -2,5 +2,5 @@ // pub mod get; // pub mod delete; -pub mod post; -// pub mod put; +// pub mod post; +pub mod put; diff --git a/server/tests/tables_data/post/mod.rs b/server/tests/tables_data/post/mod.rs index 5980e9a..0fb746c 100644 --- a/server/tests/tables_data/post/mod.rs +++ b/server/tests/tables_data/post/mod.rs @@ -1,4 +1,4 @@ // tests/tables_data/post/mod.rs -// pub mod post_table_data_test; +pub mod post_table_data_test; pub mod post_table_data_steel_decimal_test; diff --git a/server/tests/tables_data/put/mod.rs b/server/tests/tables_data/put/mod.rs index d4c3803..de8915f 100644 --- a/server/tests/tables_data/put/mod.rs +++ b/server/tests/tables_data/put/mod.rs @@ -1,3 +1,4 @@ // tests/tables_data/put/mod.rs -pub mod put_table_data_test; +// pub mod put_table_data_test; +pub mod put_table_data_steel_decimal_test; diff --git a/server/tests/tables_data/put/put_table_data_steel_decimal_test.rs b/server/tests/tables_data/put/put_table_data_steel_decimal_test.rs new file mode 100644 index 0000000..f428342 --- /dev/null +++ b/server/tests/tables_data/put/put_table_data_steel_decimal_test.rs @@ -0,0 +1,552 @@ +// tests/tables_data/put/put_table_data_steel_decimal_test.rs + +use sqlx::PgPool; +use common::proto::multieko2::{ + table_definition::{PostTableDefinitionRequest, ColumnDefinition}, + table_script::PostTableScriptRequest, + tables_data::{PostTableDataRequest, PutTableDataRequest}, +}; +use prost_types::value::Kind; +use prost_types::Value as ProtoValue; +use std::collections::HashMap; +use tokio::sync::mpsc; + +use server::indexer::IndexCommand; +use server::table_definition::handlers::post_table_definition::post_table_definition; +use server::table_script::handlers::post_table_script::post_table_script; +use server::tables_data::handlers::post_table_data::post_table_data; +use server::tables_data::handlers::put_table_data::put_table_data; + +async fn setup_test_schema(pool: &PgPool, profile_name: &str, table_name: &str) -> i64 { + // Create table definition + let request = PostTableDefinitionRequest { + profile_name: profile_name.to_string(), + table_name: table_name.to_string(), + columns: vec![ + ColumnDefinition { + name: "price".to_string(), + field_type: "decimal(10, 2)".to_string(), + }, + ColumnDefinition { + name: "quantity".to_string(), + field_type: "integer".to_string(), + }, + ColumnDefinition { + name: "total".to_string(), + field_type: "decimal(10, 2)".to_string(), + }, + ColumnDefinition { + name: "percentage".to_string(), + field_type: "decimal(28, 24)".to_string(), + }, + ], + indexes: vec![], + links: vec![], + }; + + let response = post_table_definition(pool, request).await.unwrap(); + assert!(response.success); + + // Get table definition ID + let schema_row = sqlx::query!("SELECT id FROM schemas WHERE name = $1", profile_name) + .fetch_one(pool) + .await + .unwrap(); + + let table_row = sqlx::query!( + "SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2", + schema_row.id, + table_name + ) + .fetch_one(pool) + .await + .unwrap(); + + table_row.id +} + +async fn create_indexer_channel() -> mpsc::Sender { + let (tx, mut rx) = mpsc::channel(100); + + tokio::spawn(async move { + while let Some(_) = rx.recv().await { + // Just consume messages for testing + } + }); + + tx +} + +// Helper function to create initial record and return its ID +async fn create_initial_record( + pool: &PgPool, + profile_name: &str, + table_name: &str, + indexer_tx: &mpsc::Sender, +) -> i64 { + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("10.00".to_string())), + }); + data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(1.0)), + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("10.00".to_string())), + }); + // Add percentage to satisfy all potential validation scripts during creation + data.insert("percentage".to_string(), ProtoValue { + kind: Some(Kind::StringValue("100.00".to_string())), + }); + + let data_request = PostTableDataRequest { + profile_name: profile_name.to_string(), + table_name: table_name.to_string(), + data, + }; + + let response = post_table_data(pool, data_request, indexer_tx).await.unwrap(); + response.inserted_id +} + +#[sqlx::test] +async fn test_put_basic_arithmetic_validation_success(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_put_arithmetic", "invoice").await; + let indexer_tx = create_indexer_channel().await; + + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(* $price $quantity)".to_string(), + description: "Total = Price × Quantity".to_string(), + }; + post_table_script(&pool, script_request).await.unwrap(); + + let record_id = create_initial_record(&pool, "test_put_arithmetic", "invoice", &indexer_tx).await; + + let mut update_data = HashMap::new(); + update_data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("25.50".to_string())), + }); + update_data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(3.0)), + }); + update_data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("76.50".to_string())), + }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_arithmetic".to_string(), + table_name: "invoice".to_string(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&pool, put_request, &indexer_tx).await.unwrap(); + assert!(response.success); + assert_eq!(response.updated_id, record_id); +} + +#[sqlx::test] +async fn test_put_basic_arithmetic_validation_failure(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_put_arithmetic_fail", "invoice").await; + let indexer_tx = create_indexer_channel().await; + + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(* $price $quantity)".to_string(), + description: "Total = Price × Quantity".to_string(), + }; + post_table_script(&pool, script_request).await.unwrap(); + + let record_id = create_initial_record(&pool, "test_put_arithmetic_fail", "invoice", &indexer_tx).await; + + let mut update_data = HashMap::new(); + update_data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("25.50".to_string())), + }); + update_data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(3.0)), + }); + update_data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("70.00".to_string())), + }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_arithmetic_fail".to_string(), + table_name: "invoice".to_string(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&pool, put_request, &indexer_tx).await; + assert!(result.is_err()); + + let error = result.unwrap_err(); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + let msg = error.message(); + assert!(msg.contains("Validation failed for column 'total'")); + assert!(msg.contains("Script calculated '76.5'")); + assert!(msg.contains("but user provided '70.00'")); +} + +#[sqlx::test] +async fn test_put_complex_formula_validation(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_put_complex", "order").await; + let indexer_tx = create_indexer_channel().await; + + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(+ (* $price $quantity) (* (* $price $quantity) 0.08))".to_string(), + description: "Total with 8% tax".to_string(), + }; + post_table_script(&pool, script_request).await.unwrap(); + + let record_id = create_initial_record(&pool, "test_put_complex", "order", &indexer_tx).await; + + let mut update_data = HashMap::new(); + update_data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("100.00".to_string())), + }); + update_data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(2.0)), + }); + update_data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("216.00".to_string())), + }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_complex".to_string(), + table_name: "order".to_string(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&pool, put_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_put_division_with_precision(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_put_division", "calculation").await; + let indexer_tx = create_indexer_channel().await; + + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "percentage".to_string(), + script: "(/ $total $price)".to_string(), + description: "Percentage = Total / Price".to_string(), + }; + post_table_script(&pool, script_request).await.unwrap(); + + let record_id = create_initial_record(&pool, "test_put_division", "calculation", &indexer_tx).await; + + let mut update_data = HashMap::new(); + update_data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("3.00".to_string())), + }); + update_data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("10.00".to_string())), + }); + update_data.insert("percentage".to_string(), ProtoValue { + kind: Some(Kind::StringValue("3.333333333333333333333333".to_string())), + }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_division".to_string(), + table_name: "calculation".to_string(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&pool, put_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_put_advanced_math_functions(pool: PgPool) { + let request = PostTableDefinitionRequest { + profile_name: "test_put_advanced_math".to_string(), + table_name: "calculations".to_string(), + columns: vec![ + ColumnDefinition { + name: "input".to_string(), + field_type: "decimal(10, 4)".to_string(), + }, + ColumnDefinition { + name: "square_root".to_string(), + field_type: "decimal(28, 24)".to_string(), + }, + ColumnDefinition { + name: "power_result".to_string(), + field_type: "decimal(10, 4)".to_string(), + }, + ], + indexes: vec![], + links: vec![], + }; + post_table_definition(&pool, request).await.unwrap(); + + let schema_row = sqlx::query!("SELECT id FROM schemas WHERE name = $1", "test_put_advanced_math") + .fetch_one(&pool).await.unwrap(); + let table_row = sqlx::query!( + "SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2", + schema_row.id, "calculations" + ).fetch_one(&pool).await.unwrap(); + + let indexer_tx = create_indexer_channel().await; + + let sqrt_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "square_root".to_string(), + script: "(sqrt $input)".to_string(), + description: "Square root validation".to_string(), + }; + post_table_script(&pool, sqrt_script).await.unwrap(); + + let power_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "power_result".to_string(), + script: "(^ $input 2.0)".to_string(), + description: "Power function validation".to_string(), + }; + post_table_script(&pool, power_script).await.unwrap(); + + let mut initial_data = HashMap::new(); + initial_data.insert("input".to_string(), ProtoValue { kind: Some(Kind::StringValue("4.0000".to_string())) }); + initial_data.insert("square_root".to_string(), ProtoValue { kind: Some(Kind::StringValue("2.0".to_string())) }); + initial_data.insert("power_result".to_string(), ProtoValue { kind: Some(Kind::StringValue("16.0000".to_string())) }); + let initial_request = PostTableDataRequest { + profile_name: "test_put_advanced_math".to_string(), + table_name: "calculations".to_string(), + data: initial_data, + }; + let record_id = post_table_data(&pool, initial_request, &indexer_tx).await.unwrap().inserted_id; + + let mut update_data = HashMap::new(); + update_data.insert("input".to_string(), ProtoValue { kind: Some(Kind::StringValue("16.0000".to_string())) }); + update_data.insert("square_root".to_string(), ProtoValue { kind: Some(Kind::StringValue("4.0".to_string())) }); + update_data.insert("power_result".to_string(), ProtoValue { kind: Some(Kind::StringValue("256.0000".to_string())) }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_advanced_math".to_string(), + table_name: "calculations".to_string(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&pool, put_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_put_financial_calculations(pool: PgPool) { + let request = PostTableDefinitionRequest { + profile_name: "test_put_financial".to_string(), + table_name: "finance".to_string(), + columns: vec![ + ColumnDefinition { name: "principal".to_string(), field_type: "decimal(12, 2)".to_string() }, + ColumnDefinition { name: "rate".to_string(), field_type: "decimal(6, 4)".to_string() }, + ColumnDefinition { name: "time".to_string(), field_type: "decimal(4, 1)".to_string() }, + ColumnDefinition { name: "compound_result".to_string(), field_type: "decimal(12, 4)".to_string() }, + ColumnDefinition { name: "percentage_result".to_string(), field_type: "decimal(10, 2)".to_string() }, + ], + indexes: vec![], links: vec![], + }; + post_table_definition(&pool, request).await.unwrap(); + + let schema_row = sqlx::query!("SELECT id FROM schemas WHERE name = $1", "test_put_financial").fetch_one(&pool).await.unwrap(); + let table_row = sqlx::query!("SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2", schema_row.id, "finance").fetch_one(&pool).await.unwrap(); + let indexer_tx = create_indexer_channel().await; + + let compound_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "compound_result".to_string(), + script: "(* $principal (^ (+ 1.0 $rate) $time))".to_string(), + description: "Compound interest calculation".to_string(), + }; + post_table_script(&pool, compound_script).await.unwrap(); + + let percentage_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "percentage_result".to_string(), + script: "(* $principal $rate)".to_string(), + description: "Percentage calculation".to_string(), + }; + post_table_script(&pool, percentage_script).await.unwrap(); + + let mut initial_data = HashMap::new(); + initial_data.insert("principal".to_string(), ProtoValue { kind: Some(Kind::StringValue("500.00".to_string())) }); + initial_data.insert("rate".to_string(), ProtoValue { kind: Some(Kind::StringValue("0.0300".to_string())) }); + initial_data.insert("time".to_string(), ProtoValue { kind: Some(Kind::StringValue("1.0".to_string())) }); + initial_data.insert("compound_result".to_string(), ProtoValue { kind: Some(Kind::StringValue("515.0000".to_string())) }); + initial_data.insert("percentage_result".to_string(), ProtoValue { kind: Some(Kind::StringValue("15.00".to_string())) }); + let initial_request = PostTableDataRequest { + profile_name: "test_put_financial".to_string(), + table_name: "finance".to_string(), + data: initial_data, + }; + let record_id = post_table_data(&pool, initial_request, &indexer_tx).await.unwrap().inserted_id; + + let mut update_data = HashMap::new(); + update_data.insert("principal".to_string(), ProtoValue { kind: Some(Kind::StringValue("1000.00".to_string())) }); + update_data.insert("rate".to_string(), ProtoValue { kind: Some(Kind::StringValue("0.0500".to_string())) }); + update_data.insert("time".to_string(), ProtoValue { kind: Some(Kind::StringValue("2.0".to_string())) }); + update_data.insert("compound_result".to_string(), ProtoValue { kind: Some(Kind::StringValue("1102.5000".to_string())) }); + update_data.insert("percentage_result".to_string(), ProtoValue { kind: Some(Kind::StringValue("50.00".to_string())) }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_financial".to_string(), + table_name: "finance".to_string(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&pool, put_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_put_partial_update_with_validation(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_put_partial", "invoice").await; + let indexer_tx = create_indexer_channel().await; + + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(* $price $quantity)".to_string(), + description: "Total = Price × Quantity".to_string(), + }; + post_table_script(&pool, script_request).await.unwrap(); + + let record_id = create_initial_record(&pool, "test_put_partial", "invoice", &indexer_tx).await; + + // Partial update: only update quantity. Validation for total should still run and pass. + // The merged context will be { price: 10.00, quantity: 5, total: 10.00, ... } + // The script will calculate total as 10.00 * 5 = 50.00. + // Since we are not providing 'total' in the update, validation for it is skipped. + let mut update_data = HashMap::new(); + update_data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(5.0)), + }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_partial".to_string(), + table_name: "invoice".to_string(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&pool, put_request, &indexer_tx).await.unwrap(); + assert!(response.success); + + // Now, test a partial update that SHOULD fail validation. + // We update quantity and provide an incorrect total. + let mut failing_update_data = HashMap::new(); + failing_update_data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(3.0)), + }); + failing_update_data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("99.99".to_string())), // Wrong! Should be 10.00 * 3 = 30.00 + }); + + let failing_put_request = PutTableDataRequest { + profile_name: "test_put_partial".to_string(), + table_name: "invoice".to_string(), + id: record_id, + data: failing_update_data, + }; + + let result = put_table_data(&pool, failing_put_request, &indexer_tx).await; + assert!(result.is_err()); + let error = result.unwrap_err(); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + assert!(error.message().contains("Script calculated '30'")); +} + +#[sqlx::test] +async fn test_put_no_data_update(pool: PgPool) { + let _table_def_id = setup_test_schema(&pool, "test_put_empty", "invoice").await; + let indexer_tx = create_indexer_channel().await; + + let record_id = create_initial_record(&pool, "test_put_empty", "invoice", &indexer_tx).await; + + let update_data = HashMap::new(); + + let put_request = PutTableDataRequest { + profile_name: "test_put_empty".to_string(), + table_name: "invoice".to_string(), + id: record_id, + data: update_data, + }; + + let response = put_table_data(&pool, put_request, &indexer_tx).await.unwrap(); + assert!(response.success); + assert!(response.message.contains("No fields to update")); +} + +#[sqlx::test] +async fn test_put_record_not_found(pool: PgPool) { + let _table_def_id = setup_test_schema(&pool, "test_put_not_found", "invoice").await; + let indexer_tx = create_indexer_channel().await; + + let mut update_data = HashMap::new(); + update_data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("100.00".to_string())), + }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_not_found".to_string(), + table_name: "invoice".to_string(), + id: 99999, + data: update_data, + }; + + let result = put_table_data(&pool, put_request, &indexer_tx).await; + assert!(result.is_err()); + + let error = result.unwrap_err(); + assert_eq!(error.code(), tonic::Code::NotFound); + assert!(error.message().contains("Record not found")); +} + +#[sqlx::test] +async fn test_put_steel_script_error_handling(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_put_errors", "error_test").await; + let indexer_tx = create_indexer_channel().await; + + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(/ $price 0.0)".to_string(), + description: "Error test".to_string(), + }; + post_table_script(&pool, script_request).await.unwrap(); + + let record_id = create_initial_record(&pool, "test_put_errors", "error_test", &indexer_tx).await; + + let mut update_data = HashMap::new(); + update_data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("100.00".to_string())), + }); + update_data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("999.99".to_string())), + }); + + let put_request = PutTableDataRequest { + profile_name: "test_put_errors".to_string(), + table_name: "error_test".to_string(), + id: record_id, + data: update_data, + }; + + let result = put_table_data(&pool, put_request, &indexer_tx).await; + assert!(result.is_err()); + + let error = result.unwrap_err(); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + assert!(error.message().contains("Script execution failed")); + assert!(error.message().contains("Division by zero")); +}