tests for table definitions

This commit is contained in:
filipriec
2025-06-17 22:46:04 +02:00
parent 011fafc0ff
commit 6faf0a4a31
3 changed files with 321 additions and 3 deletions

View File

@@ -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::<String, _>("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);
}