robust testing of the table definitions
This commit is contained in:
244
server/tests/table_definition/post_table_definition_test3.rs
Normal file
244
server/tests/table_definition/post_table_definition_test3.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
// tests/table_definition/post_table_definition_test3.rs
|
||||
|
||||
// NOTE: All 'use' statements have been removed from this file.
|
||||
// They are inherited from the parent file that includes this one.
|
||||
|
||||
// ========= Helper Functions for this Test File =========
|
||||
|
||||
/// Checks that a table definition does NOT exist for a given profile and table name.
|
||||
async fn assert_table_definition_does_not_exist(pool: &PgPool, profile_name: &str, table_name: &str) {
|
||||
let count: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM table_definitions td
|
||||
JOIN profiles p ON td.profile_id = p.id
|
||||
WHERE p.name = $1 AND td.table_name = $2",
|
||||
profile_name,
|
||||
table_name
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("Failed to query for table definition")
|
||||
.unwrap_or(0);
|
||||
|
||||
assert_eq!(
|
||||
count, 0,
|
||||
"Table definition for '{}/{}' was found but should have been rolled back.",
|
||||
profile_name, table_name
|
||||
);
|
||||
}
|
||||
|
||||
// ========= Category 2: Advanced Identifier and Naming Collisions =========
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_fail_on_column_name_collision_with_fk(
|
||||
#[future] pool_with_preexisting_table: PgPool,
|
||||
) {
|
||||
// Scenario: Create a table that links to 'customers' and also defines its own 'customers_id' column.
|
||||
// Expected: The generated CREATE TABLE will have a duplicate column, causing a database error.
|
||||
let pool = pool_with_preexisting_table.await; // Provides 'customers' table
|
||||
let request = PostTableDefinitionRequest {
|
||||
profile_name: "default".into(),
|
||||
table_name: "orders_collision".into(),
|
||||
columns: vec![ColumnDefinition {
|
||||
name: "customers_id".into(), // This will collide with the generated FK
|
||||
field_type: "integer".into(),
|
||||
}],
|
||||
links: vec![TableLink {
|
||||
linked_table_name: "customers".into(),
|
||||
required: true,
|
||||
}],
|
||||
indexes: vec![],
|
||||
};
|
||||
|
||||
// Act
|
||||
let result = post_table_definition(&pool, request).await;
|
||||
|
||||
// Assert
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(
|
||||
err.code(),
|
||||
Code::Internal,
|
||||
"Expected Internal error due to duplicate column in CREATE TABLE"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_fail_on_duplicate_column_names_in_request(#[future] pool: PgPool) {
|
||||
// Scenario: The request itself contains two columns with the same name.
|
||||
// Expected: Database error on CREATE TABLE with duplicate column definition.
|
||||
let pool = pool.await;
|
||||
let request = PostTableDefinitionRequest {
|
||||
profile_name: "default".into(),
|
||||
table_name: "duplicate_cols".into(),
|
||||
columns: vec![
|
||||
ColumnDefinition {
|
||||
name: "product_name".into(),
|
||||
field_type: "text".into(),
|
||||
},
|
||||
ColumnDefinition {
|
||||
name: "product_name".into(),
|
||||
field_type: "text".into(),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Act
|
||||
let result = post_table_definition(&pool, request).await;
|
||||
|
||||
// Assert
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(err.code(), Code::Internal);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_link_to_sanitized_table_name(#[future] pool: PgPool) {
|
||||
// Scenario: Test that linking requires using the sanitized name, not the original.
|
||||
let pool = pool.await;
|
||||
let original_name = "My Invoices";
|
||||
let sanitized_name = "myinvoices";
|
||||
|
||||
// 1. Create the table with a name that requires sanitization.
|
||||
let create_req = PostTableDefinitionRequest {
|
||||
profile_name: "default".into(),
|
||||
table_name: original_name.into(),
|
||||
..Default::default()
|
||||
};
|
||||
let resp = post_table_definition(&pool, create_req).await.unwrap();
|
||||
assert!(resp.sql.contains(&format!("gen.\"{}\"", sanitized_name)));
|
||||
|
||||
// 2. Attempt to link to the *original* name, which should fail.
|
||||
let link_req_fail = PostTableDefinitionRequest {
|
||||
profile_name: "default".into(),
|
||||
table_name: "payments".into(),
|
||||
links: vec![TableLink {
|
||||
linked_table_name: original_name.into(),
|
||||
required: true,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let err = post_table_definition(&pool, link_req_fail)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.code(), Code::NotFound);
|
||||
assert!(err.message().contains("Linked table My Invoices not found"));
|
||||
|
||||
// 3. Attempt to link to the *sanitized* name, which should succeed.
|
||||
let link_req_success = PostTableDefinitionRequest {
|
||||
profile_name: "default".into(),
|
||||
table_name: "payments_sanitized".into(),
|
||||
links: vec![TableLink {
|
||||
linked_table_name: sanitized_name.into(),
|
||||
required: true,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let success_resp = post_table_definition(&pool, link_req_success).await.unwrap();
|
||||
assert!(success_resp.success);
|
||||
assert!(success_resp
|
||||
.sql
|
||||
.contains(&format!("REFERENCES gen.\"{}\"(id)", sanitized_name)));
|
||||
}
|
||||
|
||||
// ========= Category 3: Complex Link and Profile Logic =========
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_fail_on_true_self_referential_link(#[future] pool: PgPool) {
|
||||
// Scenario: A table attempts to link to itself in the same request.
|
||||
// Expected: NotFound, because the table definition doesn't exist yet at link-check time.
|
||||
let pool = pool.await;
|
||||
let request = PostTableDefinitionRequest {
|
||||
profile_name: "default".into(),
|
||||
table_name: "employees".into(),
|
||||
links: vec![TableLink {
|
||||
linked_table_name: "employees".into(), // Self-reference
|
||||
required: false, // For a manager_id FK
|
||||
}],
|
||||
..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 employees not found"));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_behavior_on_empty_profile_name(#[future] pool: PgPool) {
|
||||
// Scenario: Attempt to create a table with an empty profile name.
|
||||
// Expected: This should violate the database's NOT NULL constraint on profiles.name.
|
||||
let pool = pool.await;
|
||||
let request = PostTableDefinitionRequest {
|
||||
profile_name: "".into(),
|
||||
table_name: "table_in_empty_profile".into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Act
|
||||
let result = post_table_definition(&pool, request).await;
|
||||
|
||||
// Assert
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(
|
||||
err.code(),
|
||||
Code::Internal,
|
||||
"Expected Internal error from DB constraint violation"
|
||||
);
|
||||
assert!(
|
||||
err.message().to_lowercase().contains("profile error"),
|
||||
"Unexpected error message: {}",
|
||||
err.message()
|
||||
);
|
||||
}
|
||||
|
||||
// ========= Category 4: Concurrency =========
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
#[ignore = "Concurrency tests can be flaky and require careful setup"]
|
||||
async fn test_race_condition_on_table_creation(#[future] pool: PgPool) {
|
||||
// Scenario: Two requests try to create the exact same table at the same time.
|
||||
// Expected: One succeeds, the other fails with AlreadyExists.
|
||||
let pool = pool.await;
|
||||
let request1 = PostTableDefinitionRequest {
|
||||
profile_name: "concurrent_profile".into(),
|
||||
table_name: "racy_table".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let request2 = request1.clone();
|
||||
|
||||
let pool1 = pool.clone();
|
||||
let pool2 = pool.clone();
|
||||
|
||||
// Act
|
||||
let (res1, res2) = tokio::join!(
|
||||
post_table_definition(&pool1, request1),
|
||||
post_table_definition(&pool2, request2)
|
||||
);
|
||||
|
||||
// Assert
|
||||
let results = vec![res1, res2];
|
||||
let success_count = results.iter().filter(|r| r.is_ok()).count();
|
||||
let failure_count = results.iter().filter(|r| r.is_err()).count();
|
||||
|
||||
assert_eq!(
|
||||
success_count, 1,
|
||||
"Exactly one request should succeed"
|
||||
);
|
||||
assert_eq!(failure_count, 1, "Exactly one request should fail");
|
||||
|
||||
let err = results
|
||||
.into_iter()
|
||||
.find(|r| r.is_err())
|
||||
.unwrap()
|
||||
.unwrap_err();
|
||||
assert_eq!(err.code(), Code::AlreadyExists);
|
||||
assert_eq!(err.message(), "Table already exists in this profile");
|
||||
}
|
||||
Reference in New Issue
Block a user