383 lines
12 KiB
Rust
383 lines
12 KiB
Rust
// tests/table_definition/post_table_definition_test5.rs
|
||
|
||
// NOTE: All 'use' statements are inherited from the parent file that includes this one.
|
||
|
||
// ========= Category 7: Schema Validation and Edge Cases =========
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_schema_name_validation_reserved_names(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
let reserved_names = vec![
|
||
"pg_catalog",
|
||
"information_schema",
|
||
"pg_toast",
|
||
"public", // May be reserved depending on implementation
|
||
];
|
||
|
||
for reserved_name in reserved_names {
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: reserved_name.into(),
|
||
table_name: "test_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err(), "Reserved schema name '{}' should be rejected", reserved_name);
|
||
if let Err(status) = result {
|
||
assert_eq!(status.code(), Code::InvalidArgument);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_schema_name_validation_sql_injection(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
let malicious_names = vec![
|
||
"test; DROP SCHEMA public",
|
||
"test'; DROP TABLE users; --",
|
||
"test\"; CREATE TABLE evil; --",
|
||
];
|
||
|
||
for malicious_name in malicious_names {
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: malicious_name.into(),
|
||
table_name: "test_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err(), "Malicious schema name '{}' should be rejected", malicious_name);
|
||
if let Err(status) = result {
|
||
assert_eq!(status.code(), Code::InvalidArgument);
|
||
assert!(status.message().contains("contains invalid characters"));
|
||
}
|
||
}
|
||
}
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_schema_name_length_limits(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
// Test schema name length limits (63 chars in PostgreSQL)
|
||
let long_name = "a".repeat(64);
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: long_name,
|
||
table_name: "test_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err(), "Schema names longer than 63 characters should be rejected");
|
||
if let Err(status) = result {
|
||
assert_eq!(status.code(), Code::InvalidArgument);
|
||
}
|
||
}
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_unicode_in_schema_names_rejected(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
let unicode_names = vec![
|
||
"test_😀",
|
||
"schéma",
|
||
"тест",
|
||
"测试",
|
||
];
|
||
|
||
for unicode_name in unicode_names {
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: unicode_name.into(),
|
||
table_name: "test_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err(), "Unicode schema name '{}' should be rejected", unicode_name);
|
||
if let Err(status) = result {
|
||
assert_eq!(status.code(), Code::InvalidArgument);
|
||
assert!(status.message().contains("contains invalid characters"));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========= Category 8: Foreign Key Edge Cases =========
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_fk_column_name_uniqueness_collision(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
// Create tables that would cause FK column name collisions
|
||
let req1 = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: "customers_146053".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
post_table_definition(&pool, req1).await.unwrap();
|
||
|
||
let req2 = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: "suppliers_146053".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
post_table_definition(&pool, req2).await.unwrap();
|
||
|
||
// Try to create a table linking to both - this should reveal the FK naming bug
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: "orders".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![
|
||
TableLink {
|
||
linked_table_name: "customers_146053".into(),
|
||
required: true,
|
||
},
|
||
TableLink {
|
||
linked_table_name: "suppliers_146053".into(),
|
||
required: true,
|
||
},
|
||
],
|
||
};
|
||
|
||
let result = post_table_definition(&pool, request).await;
|
||
|
||
// This test documents the current bug - both tables create "146053_id" columns
|
||
if result.is_err() {
|
||
let err = result.unwrap_err();
|
||
if err.message().contains("specified more than once") {
|
||
// This confirms the FK naming collision bug described in the analysis
|
||
assert!(err.message().contains("146053_id"));
|
||
} else {
|
||
// If it's a different error, let it fail normally
|
||
panic!("Unexpected error: {:?}", err);
|
||
}
|
||
} else {
|
||
// If this passes, the bug has been fixed
|
||
assert!(result.is_ok());
|
||
}
|
||
}
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_cross_schema_references_prevented(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
// Create table in schema A
|
||
let req_a = PostTableDefinitionRequest {
|
||
profile_name: "A".into(),
|
||
table_name: "users".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
post_table_definition(&pool, req_a).await.unwrap();
|
||
|
||
// Try to link from schema B to schema A's table
|
||
let req_b = PostTableDefinitionRequest {
|
||
profile_name: "B".into(),
|
||
table_name: "orders".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![TableLink {
|
||
linked_table_name: "users".into(), // This should not find A.users
|
||
required: true,
|
||
}],
|
||
};
|
||
|
||
let result = post_table_definition(&pool, req_b).await;
|
||
assert!(result.is_err());
|
||
assert!(result.unwrap_err().message().contains("not found"));
|
||
}
|
||
|
||
// ========= Category 9: Concurrent Operations =========
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_concurrent_schema_creation(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
use futures::future::join_all;
|
||
|
||
let futures = (0..10).map(|i| {
|
||
let pool = pool.clone();
|
||
async move {
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: format!("concurrent_schema_{}", i),
|
||
table_name: "test_table".into(),
|
||
columns: vec![ColumnDefinition {
|
||
name: "test_column".into(),
|
||
field_type: "text".into(),
|
||
}],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
post_table_definition(&pool, request).await
|
||
}
|
||
});
|
||
|
||
let results = join_all(futures).await;
|
||
assert!(results.iter().all(|r| r.is_ok()));
|
||
}
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_table_creation_with_many_foreign_keys(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
// Create several tables to link to
|
||
for i in 0..5 {
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: format!("target_table_{}", i),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
post_table_definition(&pool, request).await.unwrap();
|
||
}
|
||
|
||
// Create a table that links to all of them
|
||
let links = (0..5).map(|i| TableLink {
|
||
linked_table_name: format!("target_table_{}", i),
|
||
required: false,
|
||
}).collect();
|
||
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: "many_links_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links,
|
||
};
|
||
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_ok());
|
||
}
|
||
|
||
// ========= Category 10: Empty and Boundary Cases =========
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_empty_schema_and_table_names_rejected(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
// Test empty schema name
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "".into(),
|
||
table_name: "valid_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err());
|
||
assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);
|
||
|
||
// Test empty table name
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: "".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err());
|
||
assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);
|
||
}
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_schema_name_case_sensitivity(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
// Test that schema names are properly case-sensitive
|
||
let request1 = PostTableDefinitionRequest {
|
||
profile_name: "TestSchema".into(),
|
||
table_name: "test_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
post_table_definition(&pool, request1).await.unwrap();
|
||
|
||
// Different case should be treated as different schema
|
||
let request2 = PostTableDefinitionRequest {
|
||
profile_name: "testschema".into(),
|
||
table_name: "test_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
let result = post_table_definition(&pool, request2).await;
|
||
// Under case-insensitive profiles this must collide
|
||
assert!(result.is_err(), "Expected duplicate‐schema error");
|
||
let err = result.unwrap_err();
|
||
// pick the right code for “already exists” in your handler
|
||
assert_eq!(err.code(), Code::AlreadyExists, "{:?}", err);
|
||
}
|
||
|
||
#[rstest]
|
||
#[tokio::test]
|
||
async fn test_whitespace_in_identifiers_rejected(#[future] pool: PgPool) {
|
||
let pool = pool.await;
|
||
|
||
// Test schema name with whitespace
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "test schema".into(),
|
||
table_name: "test_table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err());
|
||
assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);
|
||
|
||
// Test table name with whitespace
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: "test table".into(),
|
||
columns: vec![],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err());
|
||
assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);
|
||
|
||
// Test column name with whitespace
|
||
let request = PostTableDefinitionRequest {
|
||
profile_name: "default".into(),
|
||
table_name: "test_table".into(),
|
||
columns: vec![ColumnDefinition {
|
||
name: "test column".into(),
|
||
field_type: "text".into(),
|
||
}],
|
||
indexes: vec![],
|
||
links: vec![],
|
||
};
|
||
let result = post_table_definition(&pool, request).await;
|
||
assert!(result.is_err());
|
||
assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);
|
||
}
|