275 lines
8.9 KiB
Rust
275 lines
8.9 KiB
Rust
// 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 schemas p ON td.schema_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: PgPool) {
|
|
let pool = pool.await;
|
|
|
|
// Use a unique table name to avoid conflicts with other tests
|
|
let unique_id = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.subsec_nanos();
|
|
let customers_table = format!("customers_collision_{}", unique_id);
|
|
let orders_table = format!("orders_collision_{}", unique_id);
|
|
|
|
// First, create the prerequisite table using the proper API
|
|
let customers_request = PostTableDefinitionRequest {
|
|
profile_name: "default".into(),
|
|
table_name: customers_table.clone(),
|
|
columns: vec![ColumnDefinition {
|
|
name: "name".into(),
|
|
field_type: "text".into(),
|
|
}],
|
|
links: vec![],
|
|
indexes: vec![],
|
|
};
|
|
|
|
// Create the customers table
|
|
let customers_result = post_table_definition(&pool, customers_request).await;
|
|
assert!(customers_result.is_ok(), "Failed to create prerequisite customers table: {:?}", customers_result);
|
|
|
|
// Now test the collision scenario
|
|
// This should fail because we're trying to create a "customers_collision_xxxxx_id" column
|
|
// while also linking to the table (which auto-generates the same foreign key)
|
|
let fk_column_name = format!("{}_id", customers_table);
|
|
let request = PostTableDefinitionRequest {
|
|
profile_name: "default".into(),
|
|
table_name: orders_table,
|
|
columns: vec![ColumnDefinition {
|
|
name: fk_column_name.clone(), // This will collide with the generated FK
|
|
field_type: "integer".into(),
|
|
}],
|
|
links: vec![TableLink {
|
|
linked_table_name: customers_table,
|
|
required: true,
|
|
}],
|
|
indexes: vec![],
|
|
};
|
|
|
|
// Act
|
|
let result = post_table_definition(&pool, request).await;
|
|
|
|
// Assert - this should now fail with InvalidArgument because of the column name validation
|
|
let err = result.unwrap_err();
|
|
assert_eq!(
|
|
err.code(),
|
|
Code::InvalidArgument,
|
|
"Expected InvalidArgument due to column name ending in _id, got: {:?}",
|
|
err
|
|
);
|
|
|
|
// FIXED: More flexible error message check
|
|
assert!(
|
|
err.message().contains("Column name") &&
|
|
err.message().contains("cannot be") &&
|
|
err.message().contains("end with '_id'"),
|
|
"Error message should mention the invalid column name: {}",
|
|
err.message()
|
|
);
|
|
}
|
|
|
|
#[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) {
|
|
let pool = pool.await;
|
|
|
|
// FIXED: Use valid table name instead of invalid one
|
|
let table_name = "my_invoices";
|
|
|
|
// 1. Create the table with a VALID name
|
|
let create_req = PostTableDefinitionRequest {
|
|
profile_name: "default".into(),
|
|
table_name: table_name.into(),
|
|
columns: vec![ColumnDefinition {
|
|
name: "amount".into(),
|
|
field_type: "text".into(),
|
|
}],
|
|
..Default::default()
|
|
};
|
|
let resp = post_table_definition(&pool, create_req).await.unwrap();
|
|
assert!(resp.sql.contains(&format!("\"default\".\"{}\"", table_name)));
|
|
|
|
// 2. Link to the correct name - should succeed
|
|
let link_req_success = PostTableDefinitionRequest {
|
|
profile_name: "default".into(),
|
|
table_name: "payments".into(),
|
|
columns: vec![ColumnDefinition {
|
|
name: "amount".into(),
|
|
field_type: "text".into(),
|
|
}],
|
|
links: vec![TableLink {
|
|
linked_table_name: table_name.into(),
|
|
required: true,
|
|
}],
|
|
..Default::default()
|
|
};
|
|
let success_resp = post_table_definition(&pool, link_req_success).await.unwrap();
|
|
assert!(success_resp.success);
|
|
}
|
|
|
|
// ========= 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 be rejected by input validation.
|
|
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::InvalidArgument, // Changed from Internal
|
|
"Expected InvalidArgument error from input validation"
|
|
);
|
|
assert!(
|
|
err.message().contains("Profile name cannot be empty"), // Updated message
|
|
"Unexpected error message: {}",
|
|
err.message()
|
|
);
|
|
}
|
|
|
|
// ========= Category 4: Concurrency =========
|
|
|
|
#[rstest]
|
|
#[tokio::test]
|
|
async fn test_race_condition_on_table_creation(#[future] pool: PgPool) {
|
|
let pool = pool.await;
|
|
|
|
// FIXED: Use unique profile and table names to avoid conflicts between test runs
|
|
let unique_id = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
|
|
let request1 = PostTableDefinitionRequest {
|
|
profile_name: format!("concurrent_profile_{}", unique_id),
|
|
table_name: "racy_table".into(),
|
|
columns: vec![ColumnDefinition {
|
|
name: "test_col".into(),
|
|
field_type: "text".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");
|
|
}
|