244 lines
8.0 KiB
Rust
244 lines
8.0 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_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!("\"default\".\"{}\"", 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 \"default\".\"{}\"(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 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]
|
|
#[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");
|
|
}
|