diff --git a/server/tests/tables_data/handlers/get_table_data_test.rs b/server/tests/tables_data/handlers/get_table_data_test.rs new file mode 100644 index 0000000..410d0df --- /dev/null +++ b/server/tests/tables_data/handlers/get_table_data_test.rs @@ -0,0 +1,414 @@ +// tests/tables_data/handlers/get_table_data_test.rs +use rstest::{fixture, rstest}; +use server::tables_data::handlers::get_table_data; +use common::proto::multieko2::tables_data::{GetTableDataRequest, GetTableDataResponse}; +use crate::common::setup_test_db; +use sqlx::{PgPool, Row}; +use tonic; +use chrono::Utc; +use serde_json::json; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +lazy_static::lazy_static! { + static ref TEST_MUTEX: Arc> = Arc::new(Mutex::new(())); +} + +#[fixture] +async fn pool() -> PgPool { + setup_test_db().await +} + +#[fixture] +async fn closed_pool(#[future] pool: PgPool) -> PgPool { + let pool = pool.await; + pool.close().await; + pool +} + +#[fixture] +async fn existing_profile(#[future] pool: PgPool) -> (PgPool, String, i64) { + let _guard = TEST_MUTEX.lock().await; + let pool = pool.await; + let profile_name = format!("TestProfile_{}", Utc::now().timestamp_nanos_opt().unwrap_or_default()); + let profile = sqlx::query!( + "INSERT INTO profiles (name) VALUES ($1) RETURNING id", + profile_name + ) + .fetch_one(&pool) + .await + .unwrap(); + (pool, profile_name, profile.id) +} + +#[fixture] +async fn existing_table( + #[future] existing_profile: (PgPool, String, i64), +) -> (PgPool, String, String) { + let _guard = TEST_MUTEX.lock().await; + let (pool, profile_name, profile_id) = existing_profile.await; + let table_name = format!("test_table_{}", Utc::now().timestamp_nanos_opt().unwrap_or_default()); + + // Define columns for the table + let columns = json!([ + "\"name\" VARCHAR(255)", + "\"age\" INTEGER", + "\"email\" VARCHAR(100)", + "\"is_active\" BOOLEAN" + ]); + + sqlx::query!( + "INSERT INTO table_definitions (profile_id, table_name, columns) VALUES ($1, $2, $3)", + profile_id, + table_name, + columns + ) + .execute(&pool) + .await + .unwrap(); + + // Create actual table + let create_table = format!( + r#" + CREATE TABLE "{}" ( + id BIGSERIAL PRIMARY KEY, + deleted BOOLEAN NOT NULL DEFAULT false, + firma TEXT NOT NULL, + name VARCHAR(255), + age INTEGER, + email VARCHAR(100), + is_active BOOLEAN + ) + "#, + table_name + ); + + sqlx::query(&create_table) + .execute(&pool) + .await + .unwrap(); + + (pool, profile_name, table_name) +} + +#[fixture] +async fn existing_record( + #[future] existing_table: (PgPool, String, String), +) -> (PgPool, String, String, i64) { + let _guard = TEST_MUTEX.lock().await; + let (pool, profile_name, table_name) = existing_table.await; + + let query = format!( + r#"INSERT INTO "{}" (firma, name, age, email, is_active) + VALUES ($1, $2, $3, $4, $5) + RETURNING id"#, + table_name + ); + + let record = sqlx::query(&query) + .bind("Test Company") + .bind("John Doe") + .bind(30) + .bind("john@example.com") + .bind(true) + .fetch_one(&pool) + .await + .unwrap(); + + let id: i64 = record.get("id"); + (pool, profile_name, table_name, id) +} + +#[fixture] +async fn existing_deleted_record( + #[future] existing_table: (PgPool, String, String), +) -> (PgPool, String, String, i64) { + let _guard = TEST_MUTEX.lock().await; + let (pool, profile_name, table_name) = existing_table.await; + + let query = format!( + r#"INSERT INTO "{}" (firma, deleted) + VALUES ($1, true) + RETURNING id"#, + table_name + ); + + let record = sqlx::query(&query) + .bind("Deleted Company") + .fetch_one(&pool) + .await + .unwrap(); + + let id: i64 = record.get("id"); + (pool, profile_name, table_name, id) +} + +#[fixture] +async fn existing_record_with_nulls( + #[future] existing_table: (PgPool, String, String), +) -> (PgPool, String, String, i64) { + let _guard = TEST_MUTEX.lock().await; + let (pool, profile_name, table_name) = existing_table.await; + + let query = format!( + r#"INSERT INTO "{}" (firma) + VALUES ($1) + RETURNING id"#, + table_name + ); + + let record = sqlx::query(&query) + .bind("Null Fields Company") + .fetch_one(&pool) + .await + .unwrap(); + + let id: i64 = record.get("id"); + (pool, profile_name, table_name, id) +} + +async fn cleanup_test_data(pool: &PgPool, table_name: &str) { + sqlx::query(&format!(r#"DROP TABLE IF EXISTS "{}" CASCADE"#, table_name)) + .execute(pool) + .await + .unwrap(); + + sqlx::query!("DELETE FROM table_definitions WHERE table_name = $1", table_name) + .execute(pool) + .await + .unwrap(); +} + +#[rstest] +#[tokio::test] +async fn test_get_table_data_success( + #[future] existing_record: (PgPool, String, String, i64), +) { + let (pool, profile_name, table_name, id) = existing_record.await; + let request = GetTableDataRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + id, + }; + + let response = get_table_data(&pool, request).await.unwrap(); + + assert_eq!(response.data["id"], id.to_string()); + assert_eq!(response.data["firma"], "Test Company"); + assert_eq!(response.data["name"], "John Doe"); + assert_eq!(response.data["age"], "30"); + assert_eq!(response.data["email"], "john@example.com"); + assert_eq!(response.data["is_active"], "true"); + assert_eq!(response.data["deleted"], "false"); + + cleanup_test_data(&pool, &table_name).await; +} + +#[rstest] +#[tokio::test] +async fn test_get_optional_fields_null( + #[future] existing_record_with_nulls: (PgPool, String, String, i64), +) { + let (pool, profile_name, table_name, id) = existing_record_with_nulls.await; + let request = GetTableDataRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + id, + }; + + let response = get_table_data(&pool, request).await.unwrap(); + + assert_eq!(response.data["name"], ""); + assert_eq!(response.data["age"], ""); + assert_eq!(response.data["email"], ""); + assert_eq!(response.data["is_active"], ""); + assert_eq!(response.data["deleted"], "false"); + + cleanup_test_data(&pool, &table_name).await; +} + +#[rstest] +#[tokio::test] +async fn test_get_nonexistent_id( + #[future] existing_table: (PgPool, String, String), +) { + let (pool, profile_name, table_name) = existing_table.await; + let request = GetTableDataRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + id: 9999, + }; + + let result = get_table_data(&pool, request).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), tonic::Code::NotFound); + + cleanup_test_data(&pool, &table_name).await; +} + +#[rstest] +#[tokio::test] +async fn test_get_deleted_record( + #[future] existing_deleted_record: (PgPool, String, String, i64), +) { + let (pool, profile_name, table_name, id) = existing_deleted_record.await; + let request = GetTableDataRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + id, + }; + + let result = get_table_data(&pool, request).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), tonic::Code::NotFound); + + cleanup_test_data(&pool, &table_name).await; +} + +#[rstest] +#[tokio::test] +async fn test_get_database_error( + #[future] closed_pool: PgPool, +) { + let closed_pool = closed_pool.await; + let request = GetTableDataRequest { + profile_name: "test".into(), + table_name: "test".into(), + id: 1, + }; + + let result = get_table_data(&closed_pool, request).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), tonic::Code::Internal); +} + +#[rstest] +#[tokio::test] +async fn test_get_special_characters( + #[future] existing_table: (PgPool, String, String), +) { + let (pool, profile_name, table_name) = existing_table.await; + + let query = format!( + r#"INSERT INTO "{}" (firma, name, email) + VALUES ($1, $2, $3) + RETURNING id"#, + table_name + ); + + let record = sqlx::query(&query) + .bind("Test Company") + .bind("Náměstí ČR") + .bind("čšěř@example.com") + .fetch_one(&pool) + .await + .unwrap(); + + let id: i64 = record.get("id"); + + let request = GetTableDataRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + id, + }; + + let response = get_table_data(&pool, request).await.unwrap(); + + assert_eq!(response.data["name"], "Náměstí ČR"); + assert_eq!(response.data["email"], "čšěř@example.com"); + + cleanup_test_data(&pool, &table_name).await; +} + +#[rstest] +#[tokio::test] +async fn test_get_max_length_fields( + #[future] existing_table: (PgPool, String, String), +) { + let (pool, profile_name, table_name) = existing_table.await; + + let long_name = "a".repeat(255); + let query = format!( + r#"INSERT INTO "{}" (firma, name) + VALUES ($1, $2) + RETURNING id"#, + table_name + ); + + let record = sqlx::query(&query) + .bind("Test Company") + .bind(&long_name) + .fetch_one(&pool) + .await + .unwrap(); + + let id: i64 = record.get("id"); + + let request = GetTableDataRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + id, + }; + + let response = get_table_data(&pool, request).await.unwrap(); + + assert_eq!(response.data["name"], long_name); + assert_eq!(response.data["name"].len(), 255); + + cleanup_test_data(&pool, &table_name).await; +} + +#[rstest] +#[tokio::test] +async fn test_get_invalid_profile( + #[future] pool: PgPool, +) { + let pool = pool.await; + let request = GetTableDataRequest { + profile_name: "non_existent_profile".into(), + table_name: "test_table".into(), + id: 1, + }; + + let result = get_table_data(&pool, request).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), tonic::Code::NotFound); +} + +#[rstest] +#[tokio::test] +async fn test_get_invalid_table( + #[future] existing_profile: (PgPool, String, i64), +) { + let (pool, profile_name, _) = existing_profile.await; + let request = GetTableDataRequest { + profile_name, + table_name: "non_existent_table".into(), + id: 1, + }; + + let result = get_table_data(&pool, request).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), tonic::Code::NotFound); +} + +#[rstest] +#[tokio::test] +async fn test_get_invalid_column( + #[future] existing_record: (PgPool, String, String, i64), +) { + let (pool, profile_name, table_name, id) = existing_record.await; + + // Try to request a column that doesn't exist in the table definition + let mut request = GetTableDataRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + id, + }; + + let result = get_table_data(&pool, request).await; + assert!(result.is_ok()); // Should still succeed as we're not filtering columns + + cleanup_test_data(&pool, &table_name).await; +} diff --git a/server/tests/tables_data/handlers/mod.rs b/server/tests/tables_data/handlers/mod.rs index 36c17f4..189ddb4 100644 --- a/server/tests/tables_data/handlers/mod.rs +++ b/server/tests/tables_data/handlers/mod.rs @@ -2,4 +2,5 @@ pub mod post_table_data_test; pub mod put_table_data_test; pub mod delete_table_data_test; +pub mod get_table_data_test;