diff --git a/server/src/table_definition/handlers/post_table_definition.rs b/server/src/table_definition/handlers/post_table_definition.rs index e563005..dd4732a 100644 --- a/server/src/table_definition/handlers/post_table_definition.rs +++ b/server/src/table_definition/handlers/post_table_definition.rs @@ -125,7 +125,7 @@ fn is_reserved_schema(schema_name: &str) -> bool { pub async fn post_table_definition( db_pool: &PgPool, - mut request: PostTableDefinitionRequest, // Changed to mutable + request: PostTableDefinitionRequest, ) -> Result { // Create owned copies of the strings after validation let profile_name = { diff --git a/server/tests/common/mod.rs b/server/tests/common/mod.rs index a0a35dd..75ed661 100644 --- a/server/tests/common/mod.rs +++ b/server/tests/common/mod.rs @@ -30,7 +30,7 @@ pub async fn setup_isolated_db() -> PgPool { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(), - rand::thread_rng() + rand::rng() .sample_iter(&Alphanumeric) .take(8) .map(char::from) diff --git a/server/tests/mod.rs b/server/tests/mod.rs index 38aa6cc..a533032 100644 --- a/server/tests/mod.rs +++ b/server/tests/mod.rs @@ -1,5 +1,5 @@ // tests/mod.rs -pub mod adresar; -// pub mod tables_data; +// pub mod adresar; +pub mod tables_data; pub mod common; -pub mod table_definition; +// pub mod table_definition; diff --git a/server/tests/tables_data/handlers/mod.rs b/server/tests/tables_data/handlers/mod.rs index 08ecdba..bf311e7 100644 --- a/server/tests/tables_data/handlers/mod.rs +++ b/server/tests/tables_data/handlers/mod.rs @@ -1,8 +1,7 @@ // tests/tables_data/mod.rs pub mod post_table_data_test; -pub mod put_table_data_test; -pub mod delete_table_data_test; -pub mod get_table_data_test; -pub mod get_table_data_count_test; -pub mod get_table_data_by_position_test; - +// pub mod put_table_data_test; +// pub mod delete_table_data_test; +// pub mod get_table_data_test; +// pub mod get_table_data_count_test; +// pub mod get_table_data_by_position_test; diff --git a/server/tests/tables_data/handlers/post_table_data_test.rs b/server/tests/tables_data/handlers/post_table_data_test.rs index 8fbed62..eede0a5 100644 --- a/server/tests/tables_data/handlers/post_table_data_test.rs +++ b/server/tests/tables_data/handlers/post_table_data_test.rs @@ -2,23 +2,158 @@ use rstest::{fixture, rstest}; use sqlx::PgPool; use std::collections::HashMap; +use prost_types::Value; +use prost_types::value::Kind; use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse}; +use common::proto::multieko2::table_definition::{ + PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition +}; use server::tables_data::handlers::post_table_data; +use server::table_definition::handlers::post_table_definition; use crate::common::setup_test_db; use tonic; use chrono::Utc; +use tokio::sync::mpsc; +use server::indexer::IndexCommand; +use sqlx::Row; +use rand::distr::Alphanumeric; +use rand::Rng; + +// Helper function to generate unique identifiers for test isolation +fn generate_unique_id() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(8) + .map(char::from) + .collect::() + .to_lowercase() +} + +// Helper function to convert string to protobuf Value +fn string_to_proto_value(s: String) -> Value { + Value { + kind: Some(Kind::StringValue(s)), + } +} + +// Helper function to convert HashMap to HashMap +fn convert_to_proto_values(data: HashMap) -> HashMap { + data.into_iter() + .map(|(k, v)| (k, string_to_proto_value(v))) + .collect() +} + +// Create the table definition for adresar test with unique name +async fn create_adresar_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: "firma".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "kz".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "drc".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "ulica".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "psc".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "mesto".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "stat".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "banka".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "ucet".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "skladm".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "ico".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "kontakt".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "telefon".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "skladu".into(), + field_type: "text".into(), + }, + TableColumnDefinition { + name: "fax".into(), + field_type: "text".into(), + }, + ], + indexes: vec![], + links: vec![], + }; + + post_table_definition(pool, table_def_request).await?; + Ok(()) +} + +// Test context structure to hold unique identifiers +#[derive(Clone)] +struct TestContext { + pool: PgPool, + profile_name: String, + table_name: String, + indexer_tx: mpsc::Sender, +} // Fixtures #[fixture] -async fn pool() -> PgPool { - setup_test_db().await +async fn test_context() -> TestContext { + let pool = setup_test_db().await; + let unique_id = generate_unique_id(); + let profile_name = format!("test_profile_{}", unique_id); + let table_name = format!("adresar_test_{}", unique_id); + + // Create the table for this specific test + create_adresar_table(&pool, &table_name, &profile_name).await + .expect("Failed to create test table"); + + let (tx, _rx) = mpsc::channel(100); + + TestContext { + pool, + profile_name, + table_name, + indexer_tx: tx, + } } #[fixture] -async fn closed_pool(#[future] pool: PgPool) -> PgPool { - let pool = pool.await; - pool.close().await; - pool +async fn closed_test_context() -> TestContext { + let mut context = test_context().await; + context.pool.close().await; + context } #[fixture] @@ -49,43 +184,38 @@ fn minimal_request() -> HashMap { map } -fn create_table_request(data: HashMap) -> PostTableDataRequest { +fn create_table_request(context: &TestContext, data: HashMap) -> PostTableDataRequest { PostTableDataRequest { - profile_name: "default".into(), - table_name: "2025_adresar".into(), - data, + profile_name: context.profile_name.clone(), + table_name: context.table_name.clone(), + data: convert_to_proto_values(data), } } -async fn assert_table_response(pool: &PgPool, response: &PostTableDataResponse, expected: &HashMap) { - let row = sqlx::query!(r#"SELECT * FROM "2025_adresar" WHERE id = $1"#, response.inserted_id) - .fetch_one(pool) +async fn assert_table_response(context: &TestContext, response: &PostTableDataResponse, expected: &HashMap) { + // Use dynamic query since table is created at runtime with unique names + let query = format!( + r#"SELECT * FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) .await .unwrap(); - assert_eq!(row.firma, expected["firma"]); - assert!(!row.deleted); + // Get values from Row dynamically + let firma: String = row.get("firma"); + let deleted: bool = row.get("deleted"); - // Check optional fields using direct struct access - let check_field = |field: &str, value: &str| { - let db_value = match field { - "kz" => row.kz.as_deref().unwrap_or_default(), - "drc" => row.drc.as_deref().unwrap_or_default(), - "ulica" => row.ulica.as_deref().unwrap_or_default(), - "psc" => row.psc.as_deref().unwrap_or_default(), - "mesto" => row.mesto.as_deref().unwrap_or_default(), - "stat" => row.stat.as_deref().unwrap_or_default(), - "banka" => row.banka.as_deref().unwrap_or_default(), - "ucet" => row.ucet.as_deref().unwrap_or_default(), - "skladm" => row.skladm.as_deref().unwrap_or_default(), - "ico" => row.ico.as_deref().unwrap_or_default(), - "kontakt" => row.kontakt.as_deref().unwrap_or_default(), - "telefon" => row.telefon.as_deref().unwrap_or_default(), - "skladu" => row.skladu.as_deref().unwrap_or_default(), - "fax" => row.fax.as_deref().unwrap_or_default(), - _ => panic!("Unexpected field: {}", field), - }; - assert_eq!(db_value, value); + assert_eq!(firma, expected["firma"]); + assert!(!deleted); + + // Check optional fields + let check_field = |field: &str, expected_value: &str| { + let db_value: Option = row.get(field); + assert_eq!(db_value.as_deref().unwrap_or(""), expected_value); }; check_field("kz", expected.get("kz").unwrap_or(&String::new())); @@ -104,82 +234,94 @@ async fn assert_table_response(pool: &PgPool, response: &PostTableDataResponse, check_field("fax", expected.get("fax").unwrap_or(&String::new())); // Handle timestamp conversion - let odt = row.created_at.unwrap(); - let created_at = chrono::DateTime::from_timestamp(odt.unix_timestamp(), odt.nanosecond()) - .expect("Invalid timestamp"); - assert!(created_at <= Utc::now()); + let created_at: Option> = row.get("created_at"); + assert!(created_at.unwrap() <= Utc::now()); } #[rstest] #[tokio::test] async fn test_create_table_data_success( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; - let request = create_table_request(valid_request.clone()); - let response = post_table_data(&pool, request).await.unwrap(); + let context = test_context.await; + let request = create_table_request(&context, valid_request.clone()); + let response = post_table_data(&context.pool, request, &context.indexer_tx).await.unwrap(); assert!(response.inserted_id > 0); assert!(response.success); assert_eq!(response.message, "Data inserted successfully"); - assert_table_response(&pool, &response, &valid_request).await; + assert_table_response(&context, &response, &valid_request).await; } -// Remaining tests follow the same pattern with fixed parameter declarations #[rstest] #[tokio::test] async fn test_create_table_data_whitespace_trimming( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = valid_request; request.insert("firma".into(), " Test Company ".into()); request.insert("telefon".into(), " +421123456789 ".into()); - let response = post_table_data(&pool, create_table_request(request)).await.unwrap(); + let response = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await.unwrap(); - let row = sqlx::query!(r#"SELECT firma, telefon FROM "2025_adresar" WHERE id = $1"#, response.inserted_id) - .fetch_one(&pool) + let query = format!( + r#"SELECT firma, telefon FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) .await .unwrap(); - assert_eq!(row.firma, "Test Company"); - assert_eq!(row.telefon.unwrap(), "+421123456789"); + let firma: String = row.get("firma"); + let telefon: Option = row.get("telefon"); + + assert_eq!(firma, "Test Company"); + assert_eq!(telefon.unwrap(), "+421123456789"); } #[rstest] #[tokio::test] async fn test_create_table_data_empty_optional_fields( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = valid_request; request.insert("telefon".into(), " ".into()); - let response = post_table_data(&pool, create_table_request(request)).await.unwrap(); - let telefon: Option = sqlx::query_scalar!(r#"SELECT telefon FROM "2025_adresar" WHERE id = $1"#, response.inserted_id) - .fetch_one(&pool) + let response = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await.unwrap(); + + let query = format!( + r#"SELECT telefon FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + + let telefon: Option = sqlx::query_scalar(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) .await .unwrap(); assert!(telefon.is_none()); } -// Fixed parameter declarations for remaining tests #[rstest] #[tokio::test] async fn test_create_table_data_invalid_firma( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = valid_request; request.insert("firma".into(), " ".into()); - let result = post_table_data(&pool, create_table_request(request)).await; + let result = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await; assert!(result.is_err()); assert_eq!(result.unwrap_err().code(), tonic::Code::InvalidArgument); } @@ -187,26 +329,26 @@ async fn test_create_table_data_invalid_firma( #[rstest] #[tokio::test] async fn test_create_table_data_minimal_request( - #[future] pool: PgPool, + #[future] test_context: TestContext, minimal_request: HashMap, ) { - let pool = pool.await; - let response = post_table_data(&pool, create_table_request(minimal_request.clone())).await.unwrap(); + let context = test_context.await; + let response = post_table_data(&context.pool, create_table_request(&context, minimal_request.clone()), &context.indexer_tx).await.unwrap(); assert!(response.inserted_id > 0); - assert_table_response(&pool, &response, &minimal_request).await; + assert_table_response(&context, &response, &minimal_request).await; } #[rstest] #[tokio::test] async fn test_create_table_data_telefon_length_limit( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = valid_request; request.insert("telefon".into(), "1".repeat(16)); - let result = post_table_data(&pool, create_table_request(request)).await; + let result = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await; assert!(result.is_err()); assert_eq!(result.unwrap_err().code(), tonic::Code::Internal); } @@ -214,46 +356,53 @@ async fn test_create_table_data_telefon_length_limit( #[rstest] #[tokio::test] async fn test_create_table_data_special_characters( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = valid_request; request.insert("ulica".into(), "Náměstí 28. října 123/456".into()); - let response = post_table_data(&pool, create_table_request(request)).await.unwrap(); - let row = sqlx::query!(r#"SELECT ulica FROM "2025_adresar" WHERE id = $1"#, response.inserted_id) - .fetch_one(&pool) + let response = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await.unwrap(); + + let query = format!( + r#"SELECT ulica FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) .await .unwrap(); - assert_eq!(row.ulica.unwrap(), "Náměstí 28. října 123/456"); + let ulica: Option = row.get("ulica"); + assert_eq!(ulica.unwrap(), "Náměstí 28. října 123/456"); } #[rstest] #[tokio::test] async fn test_create_table_data_database_error( - #[future] closed_pool: PgPool, + #[future] closed_test_context: TestContext, minimal_request: HashMap, ) { - let closed_pool = closed_pool.await; - let result = post_table_data(&closed_pool, create_table_request(minimal_request)).await; + let context = closed_test_context.await; + let result = post_table_data(&context.pool, create_table_request(&context, minimal_request), &context.indexer_tx).await; assert!(result.is_err()); assert_eq!(result.unwrap_err().code(), tonic::Code::Internal); } - #[rstest] #[tokio::test] async fn test_create_table_data_empty_firma( - #[future] pool: PgPool, + #[future] test_context: TestContext, minimal_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = minimal_request; request.insert("firma".into(), "".into()); - let result = post_table_data(&pool, create_table_request(request)).await; + let result = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await; assert!(result.is_err()); assert_eq!(result.unwrap_err().code(), tonic::Code::InvalidArgument); } @@ -261,16 +410,23 @@ async fn test_create_table_data_empty_firma( #[rstest] #[tokio::test] async fn test_create_table_data_optional_fields_null_vs_empty( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = valid_request; request.insert("telefon".into(), "".into()); - let response = post_table_data(&pool, create_table_request(request)).await.unwrap(); - let telefon: Option = sqlx::query_scalar!(r#"SELECT telefon FROM "2025_adresar" WHERE id = $1"#, response.inserted_id) - .fetch_one(&pool) + let response = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await.unwrap(); + + let query = format!( + r#"SELECT telefon FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + + let telefon: Option = sqlx::query_scalar(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) .await .unwrap(); @@ -280,20 +436,71 @@ async fn test_create_table_data_optional_fields_null_vs_empty( #[rstest] #[tokio::test] async fn test_create_table_data_field_length_limits( - #[future] pool: PgPool, + #[future] test_context: TestContext, valid_request: HashMap, ) { - let pool = pool.await; + let context = test_context.await; let mut request = valid_request; request.insert("firma".into(), "a".repeat(255)); request.insert("telefon".into(), "1".repeat(15)); // Within limits - let response = post_table_data(&pool, create_table_request(request)).await.unwrap(); - let row = sqlx::query!(r#"SELECT firma, telefon FROM "2025_adresar" WHERE id = $1"#, response.inserted_id) - .fetch_one(&pool) + let response = post_table_data(&context.pool, create_table_request(&context, request), &context.indexer_tx).await.unwrap(); + + let query = format!( + r#"SELECT firma, telefon FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) .await .unwrap(); - assert_eq!(row.firma.len(), 255); - assert_eq!(row.telefon.unwrap().len(), 15); + let firma: String = row.get("firma"); + let telefon: Option = row.get("telefon"); + + assert_eq!(firma.len(), 255); + assert_eq!(telefon.unwrap().len(), 15); +} + +#[rstest] +#[tokio::test] +async fn test_create_table_data_with_null_values( + #[future] test_context: TestContext, +) { + let context = test_context.await; + + // Create a request with some null values + let mut data = HashMap::new(); + data.insert("firma".into(), string_to_proto_value("Test Company".into())); + data.insert("telefon".into(), Value { kind: Some(Kind::NullValue(0)) }); // Explicit null + data.insert("ulica".into(), Value { kind: None }); // Another way to represent null + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.table_name.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &context.indexer_tx).await.unwrap(); + + let query = format!( + r#"SELECT firma, telefon, ulica FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.table_name + ); + + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let firma: String = row.get("firma"); + let telefon: Option = row.get("telefon"); + let ulica: Option = row.get("ulica"); + + assert_eq!(firma, "Test Company"); + assert!(telefon.is_none()); + assert!(ulica.is_none()); }