diff --git a/server/tests/mod.rs b/server/tests/mod.rs index 66b4d45..39e3317 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; diff --git a/server/tests/table_definition/mod.rs b/server/tests/table_definition/mod.rs new file mode 100644 index 0000000..f2f7972 --- /dev/null +++ b/server/tests/table_definition/mod.rs @@ -0,0 +1,3 @@ +// server/tests/table_definition/mod.rs + +pub mod post_table_definition_test; diff --git a/server/tests/table_definition/post_table_definition_test.rs b/server/tests/table_definition/post_table_definition_test.rs new file mode 100644 index 0000000..0dfc1c2 --- /dev/null +++ b/server/tests/table_definition/post_table_definition_test.rs @@ -0,0 +1,315 @@ +// tests/table_definition/post_table_definition_test.rs +use crate::common::setup_test_db; +use common::proto::multieko2::table_definition::{ + ColumnDefinition, PostTableDefinitionRequest, TableLink, +}; +use rstest::{fixture, rstest}; +use server::table_definition::handlers::post_table_definition; +use sqlx::{PgPool, Row}; +use tonic::Code; + +// ========= Fixtures ========= + +#[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 +} + +/// A fixture that creates a pre-existing 'customers' table definition, +/// so we can test linking to it. +#[fixture] +async fn pool_with_preexisting_table(#[future] pool: PgPool) -> PgPool { + let pool = pool.await; + let create_customers_req = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "customers".into(), + columns: vec![ColumnDefinition { + name: "customer_name".into(), + field_type: "text".into(), + }], + indexes: vec!["customer_name".into()], + links: vec![], + }; + post_table_definition(&pool, create_customers_req) + .await + .expect("Failed to create pre-requisite 'customers' table"); + pool +} + +// ========= Helper Functions ========= + +/// Checks the PostgreSQL information_schema to verify a table and its columns exist. +async fn assert_table_structure_is_correct( + pool: &PgPool, + table_name: &str, + expected_cols: &[(&str, &str)], +) { + let table_exists = sqlx::query_scalar::<_, bool>( + "SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'gen' AND table_name = $1 + )", + ) + .bind(table_name) + .fetch_one(pool) + .await + .unwrap(); + + assert!(table_exists, "Table 'gen.{}' was not created", table_name); + + for (col_name, col_type) in expected_cols { + let record = sqlx::query( + "SELECT data_type FROM information_schema.columns + WHERE table_schema = 'gen' AND table_name = $1 AND column_name = $2", + ) + .bind(table_name) + .bind(col_name) + .fetch_optional(pool) + .await + .unwrap(); + + let found_type = record.unwrap_or_else(|| panic!("Column '{}' not found in table '{}'", col_name, table_name)).get::("data_type"); + + // Handle type mappings, e.g., TEXT -> character varying, NUMERIC -> numeric + let normalized_found_type = found_type.to_lowercase(); + let normalized_expected_type = col_type.to_lowercase(); + + assert!( + normalized_found_type.contains(&normalized_expected_type), + "Column '{}' has wrong type. Expected: {}, Found: {}", + col_name, + col_type, + found_type + ); + } +} + +// ========= Tests ========= + +#[rstest] +#[tokio::test] +async fn test_create_table_success(#[future] pool: PgPool) { + // Arrange + let pool = pool.await; + let request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "invoices".into(), + columns: vec![ + ColumnDefinition { + name: "invoice_number".into(), + field_type: "text".into(), + }, + ColumnDefinition { + name: "amount".into(), + field_type: "decimal(10, 2)".into(), + }, + ], + indexes: vec!["invoice_number".into()], + links: vec![], + }; + + // Act + let response = post_table_definition(&pool, request).await.unwrap(); + + // Assert + assert!(response.success); + assert!(response.sql.contains("CREATE TABLE gen.\"invoices\"")); + assert!(response.sql.contains("\"invoice_number\" TEXT")); + assert!(response.sql.contains("\"amount\" NUMERIC(10, 2)")); + assert!(response + .sql + .contains("CREATE INDEX \"idx_invoices_invoice_number\"")); + + // Verify actual DB state + assert_table_structure_is_correct( + &pool, + "invoices", + &[ + ("id", "bigint"), + ("deleted", "boolean"), + ("invoice_number", "text"), + ("amount", "numeric"), + ("created_at", "timestamp with time zone"), + ], + ) + .await; +} + +#[rstest] +#[tokio::test] +async fn test_create_table_with_link( + #[future] pool_with_preexisting_table: PgPool, +) { + // Arrange + let pool = pool_with_preexisting_table.await; + let request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "orders".into(), + columns: vec![], + indexes: vec![], + links: vec![TableLink { // CORRECTED + linked_table_name: "customers".into(), + required: true, + }], + }; + + // Act + let response = post_table_definition(&pool, request).await.unwrap(); + + // Assert + assert!(response.success); + assert!(response.sql.contains( + "\"customers_id\" BIGINT NOT NULL REFERENCES gen.\"customers\"(id)" + )); + assert!(response + .sql + .contains("CREATE INDEX \"idx_orders_customers_fk\"")); + + // Verify actual DB state + assert_table_structure_is_correct( + &pool, + "orders", + &[("customers_id", "bigint")], + ) + .await; +} + +#[rstest] +#[tokio::test] +async fn test_fail_on_duplicate_table_name(#[future] pool: PgPool) { + // Arrange + let pool = pool.await; + let request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "reused_name".into(), + ..Default::default() + }; + // Create it once + post_table_definition(&pool, request.clone()).await.unwrap(); + + // Act: Try to create it again + let result = post_table_definition(&pool, request).await; + + // Assert + let err = result.unwrap_err(); + assert_eq!(err.code(), Code::AlreadyExists); + assert_eq!(err.message(), "Table already exists in this profile"); +} + +#[rstest] +#[tokio::test] +async fn test_fail_on_invalid_table_name(#[future] pool: PgPool) { + let pool = pool.await; + let mut request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "ends_with_id".into(), // Invalid name + ..Default::default() + }; + + let result = post_table_definition(&pool, request.clone()).await; + assert_eq!(result.unwrap_err().code(), Code::InvalidArgument); + + request.table_name = "deleted".into(); // Reserved name + let result = post_table_definition(&pool, request.clone()).await; + assert_eq!(result.unwrap_err().code(), Code::InvalidArgument); +} + +#[rstest] +#[tokio::test] +async fn test_fail_on_invalid_column_type(#[future] pool: PgPool) { + // Arrange + let pool = pool.await; + let request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "bad_col_type".into(), + columns: vec![ColumnDefinition { + name: "some_col".into(), + field_type: "super_string_9000".into(), // Invalid type + }], + ..Default::default() + }; + + // Act + let result = post_table_definition(&pool, request).await; + + // Assert + let err = result.unwrap_err(); + assert_eq!(err.code(), Code::InvalidArgument); + assert!(err.message().contains("Invalid field type")); +} + +#[rstest] +#[tokio::test] +async fn test_fail_on_index_for_nonexistent_column(#[future] pool: PgPool) { + // Arrange + let pool = pool.await; + let request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "bad_index".into(), + columns: vec![ColumnDefinition { + name: "real_column".into(), + field_type: "text".into(), + }], + indexes: vec!["fake_column".into()], // Index on a column not in the list + ..Default::default() + }; + + // Act + let result = post_table_definition(&pool, request).await; + + // Assert + let err = result.unwrap_err(); + assert_eq!(err.code(), Code::InvalidArgument); + assert!(err.message().contains("Index column fake_column not found")); +} + +#[rstest] +#[tokio::test] +async fn test_fail_on_link_to_nonexistent_table(#[future] pool: PgPool) { + // Arrange + let pool = pool.await; + let request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "bad_link".into(), + links: vec![TableLink { // CORRECTED + linked_table_name: "i_do_not_exist".into(), + required: false, + }], + ..Default::default() + }; + + // Act + let result = post_table_definition(&pool, request).await; + + // Assert + let err = result.unwrap_err(); + assert_eq!(err.code(), Code::NotFound); + assert!(err.message().contains("Linked table i_do_not_exist not found")); +} + +#[rstest] +#[tokio::test] +async fn test_database_error_on_closed_pool( + #[future] closed_pool: PgPool, +) { + // Arrange + let pool = closed_pool.await; + let request = PostTableDefinitionRequest { + profile_name: "default".into(), + table_name: "wont_be_created".into(), + ..Default::default() + }; + + // Act + let result = post_table_definition(&pool, request).await; + + // Assert + assert_eq!(result.unwrap_err().code(), Code::Internal); +}