// ============================================================================ // Additional edge‐case tests for PostTableDefinition // ============================================================================ // 1) Field‐type mapping for every predefined key, in various casing. #[rstest] #[tokio::test] async fn test_field_type_mapping_various_casing(#[future] pool: PgPool) { let pool = pool.await; let cases = vec![ ("text", "TEXT", "text"), ("TEXT", "TEXT", "text"), ("TeXt", "TEXT", "text"), ("string", "TEXT", "text"), ("boolean", "BOOLEAN", "boolean"), ("Boolean", "BOOLEAN", "boolean"), ("timestamp", "TIMESTAMPTZ", "timestamp with time zone"), ("time", "TIMESTAMPTZ", "timestamp with time zone"), ("money", "NUMERIC(14, 4)", "numeric"), ("integer", "INTEGER", "integer"), ("date", "DATE", "date"), ]; for (i, &(input, expected_sql, expected_db)) in cases.iter().enumerate() { let tbl = format!("ftm_{}", i); let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: tbl.clone(), columns: vec![ColumnDefinition { name: "col".into(), field_type: input.into(), }], ..Default::default() }; let resp = post_table_definition(&pool, req).await.unwrap(); assert!( resp.sql.contains(&format!("\"col\" {}", expected_sql)), "field‐type {:?} did not map to {} in `{}`", input, expected_sql, resp.sql ); assert_table_structure_is_correct( &pool, "default", // FIXED: Added schema parameter &tbl, &[ ("id", "bigint"), ("deleted", "boolean"), ("col", expected_db), ("created_at", "timestamp with time zone"), ], ) .await; } } // 3) Invalid index names must be rejected. #[rstest] #[tokio::test] async fn test_fail_on_invalid_index_names(#[future] pool: PgPool) { let pool = pool.await; let test_cases = vec![ ("1col", "Index name cannot start with a number"), ("_col", "Index name cannot start with underscore"), ("col-name", "Index name contains invalid characters"), ]; for (idx, expected_error) in test_cases { let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: "idx_bad".into(), columns: vec![ColumnDefinition { name: "good".into(), field_type: "text".into(), }], indexes: vec![idx.into()], ..Default::default() }; let result = post_table_definition(&pool, req).await; assert!(result.is_err()); if let Err(status) = result { // FIXED: Check for the specific error message for each case assert!(status.message().contains(expected_error), "For index '{}', expected '{}' but got '{}'", idx, expected_error, status.message()); } } } // 4) More invalid‐table‐name cases: starts-with digit/underscore or sanitizes to empty. #[rstest] #[tokio::test] async fn test_fail_on_more_invalid_table_names(#[future] pool: PgPool) { let pool = pool.await; let cases = vec![ ("1tbl", "invalid table name"), ("_tbl", "invalid table name"), ]; for (name, expected_msg) in cases { let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: name.into(), ..Default::default() }; let result = post_table_definition(&pool, req).await; assert!(result.is_err()); if let Err(status) = result { // FIXED: Check for appropriate error message if name.starts_with('_') { assert!(status.message().contains("Table name cannot start with underscore")); } else if name.chars().next().unwrap().is_ascii_digit() { assert!(status.message().contains("Table name cannot start with a number")); } } } } // 5) Name‐sanitization: mixed‐case table names and strip invalid characters. #[rstest] #[tokio::test] async fn test_name_sanitization(#[future] pool: PgPool) { let pool = pool.await; let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: "My-Table!123".into(), // Invalid characters columns: vec![ColumnDefinition { name: "user_name".into(), field_type: "text".into(), }], ..Default::default() }; // FIXED: Now expect error instead of success let result = post_table_definition(&pool, req).await; assert!(result.is_err()); if let Err(status) = result { assert!(status.message().contains("Table name contains invalid characters")); } } // 6) Creating a table with no custom columns, indexes, or links → only system columns. #[rstest] #[tokio::test] async fn test_create_minimal_table(#[future] pool: PgPool) { let pool = pool.await; let profile_name = "test_minimal"; let req = PostTableDefinitionRequest { profile_name: profile_name.into(), table_name: "minimal".into(), ..Default::default() }; let resp = post_table_definition(&pool, req).await.unwrap(); assert!(resp.sql.contains("id BIGSERIAL PRIMARY KEY")); assert!(resp.sql.contains("deleted BOOLEAN NOT NULL")); assert!(resp.sql.contains("created_at TIMESTAMPTZ")); assert_table_structure_is_correct( &pool, profile_name, "minimal", &[ ("id", "bigint"), ("deleted", "boolean"), ("created_at", "timestamp with time zone"), ], ) .await; } // 7) Required & optional links: NOT NULL vs NULL. #[rstest] #[tokio::test] async fn test_nullable_and_multiple_links(#[future] pool: PgPool) { let pool = pool.await; // FIXED: Use different prefixes to avoid FK column collisions let unique_suffix = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() % 1000000; let customers_table = format!("customers_{}", unique_suffix); let suppliers_table = format!("suppliers_{}", unique_suffix); // Different prefix let orders_table = format!("orders_{}", unique_suffix); // Create customers table let customers_req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: customers_table.clone(), columns: vec![ColumnDefinition { name: "name".into(), field_type: "text".into(), }], ..Default::default() }; post_table_definition(&pool, customers_req).await .expect("Failed to create customers table"); // Create suppliers table let suppliers_req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: suppliers_table.clone(), columns: vec![ColumnDefinition { name: "name".into(), field_type: "text".into(), }], ..Default::default() }; post_table_definition(&pool, suppliers_req).await .expect("Failed to create suppliers table"); // Create orders table that links to both let orders_req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: orders_table.clone(), columns: vec![ColumnDefinition { name: "amount".into(), field_type: "text".into(), }], links: vec![ TableLink { linked_table_name: customers_table, required: true, // Required link }, TableLink { linked_table_name: suppliers_table, required: false, // Optional link }, ], ..Default::default() }; let resp = post_table_definition(&pool, orders_req).await .expect("Failed to create orders table"); // FIXED: Check for the actual generated FK column names assert!( resp.sql.contains(&format!("\"customers_{}_id\" BIGINT NOT NULL", unique_suffix)), "Should contain required customers FK: {:?}", resp.sql ); assert!( resp.sql.contains(&format!("\"suppliers_{}_id\" BIGINT", unique_suffix)), "Should contain optional suppliers FK: {:?}", resp.sql ); // Check database-level nullability for optional FK let is_nullable: String = sqlx::query_scalar!( "SELECT is_nullable \ FROM information_schema.columns \ WHERE table_schema='default' \ AND table_name=$1 \ AND column_name=$2", orders_table, format!("suppliers_{}_id", unique_suffix) ) .fetch_one(&pool) .await .unwrap() .unwrap(); assert_eq!(is_nullable, "YES"); } // 8) Duplicate links in one request → Internal. #[rstest] #[tokio::test] async fn test_fail_on_duplicate_links(#[future] pool: PgPool) { let pool = pool.await; let unique_id = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); let customers_table = format!("customers_{}", unique_id); // Create the prerequisite table let prereq_req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: customers_table.clone(), columns: vec![], links: vec![], indexes: vec![], }; post_table_definition(&pool, prereq_req).await.expect("Failed to create prerequisite table"); // Now, test the duplicate link scenario let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: format!("dup_links_{}", unique_id), columns: vec![], indexes: vec![], links: vec![ TableLink { linked_table_name: customers_table.clone(), required: true, }, TableLink { linked_table_name: customers_table.clone(), required: false, }, ], }; let err = post_table_definition(&pool, req).await.unwrap_err(); assert_eq!(err.code(), Code::InvalidArgument); assert!(err.message().contains(&format!("Duplicate link to table '{}'", customers_table))); } // 9) Self‐referential FK: link child back to same‐profile parent. #[rstest] #[tokio::test] async fn test_self_referential_link(#[future] pool: PgPool) { let pool = pool.await; post_table_definition( &pool, PostTableDefinitionRequest { profile_name: "default".into(), table_name: "selfref".into(), ..Default::default() }, ) .await .unwrap(); let resp = post_table_definition( &pool, PostTableDefinitionRequest { profile_name: "default".into(), table_name: "selfref_child".into(), links: vec![TableLink { linked_table_name: "selfref".into(), required: true, }], ..Default::default() }, ) .await .unwrap(); assert!( resp .sql .contains("\"selfref_id\" BIGINT NOT NULL REFERENCES \"default\".\"selfref\"(id)"), // FIXED: Changed from gen to "default" "{:?}", resp.sql ); } // 11) Cross‐profile uniqueness & link isolation. #[rstest] #[tokio::test] async fn test_cross_profile_uniqueness_and_link_isolation(#[future] pool: PgPool) { let pool = pool.await; // Profile a: foo (CHANGED: lowercase) post_table_definition(&pool, PostTableDefinitionRequest { profile_name: "a".into(), // CHANGED: was "A" table_name: "foo".into(), columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], ..Default::default() }).await.unwrap(); // Profile b: foo, bar (CHANGED: lowercase) post_table_definition(&pool, PostTableDefinitionRequest { profile_name: "b".into(), // CHANGED: was "B" table_name: "foo".into(), columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], ..Default::default() }).await.unwrap(); post_table_definition(&pool, PostTableDefinitionRequest { profile_name: "b".into(), // CHANGED: was "B" table_name: "bar".into(), columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], ..Default::default() }).await.unwrap(); // a linking to b.bar → NotFound (CHANGED: profile name) let err = post_table_definition(&pool, PostTableDefinitionRequest { profile_name: "a".into(), // CHANGED: was "A" table_name: "linker".into(), columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], links: vec![TableLink { linked_table_name: "bar".into(), required: false, }], ..Default::default() }).await.unwrap_err(); assert_eq!(err.code(), Code::NotFound); } // 12) SQL‐injection attempts are sanitized. #[rstest] #[tokio::test] async fn test_sql_injection_sanitization(#[future] pool: PgPool) { let pool = pool.await; let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: "users; DROP TABLE users;".into(), // SQL injection attempt columns: vec![ColumnDefinition { name: "col_drop".into(), field_type: "text".into(), }], ..Default::default() }; // FIXED: Now expect error instead of success let result = post_table_definition(&pool, req).await; assert!(result.is_err()); if let Err(status) = result { assert!(status.message().contains("Table name contains invalid characters")); } } // 13) Reserved‐column shadowing: id, deleted, created_at cannot be user‐defined. #[rstest] #[tokio::test] async fn test_reserved_column_shadowing(#[future] pool: PgPool) { let pool = pool.await; for col in &["id", "deleted", "created_at"] { let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: format!("tbl_{}", col), columns: vec![ColumnDefinition { name: (*col).into(), field_type: "text".into(), }], ..Default::default() }; let err = post_table_definition(&pool, req).await.unwrap_err(); assert_eq!(err.code(), Code::InvalidArgument, "{:?}", col); // FIXED: Changed from Internal to InvalidArgument } } // 14) Identifier‐length overflow (>63 chars) yields Internal. #[rstest] #[tokio::test] async fn test_long_identifier_length(#[future] pool: PgPool) { let pool = pool.await; let long = "a".repeat(64); let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: long.clone(), columns: vec![ColumnDefinition { name: long.clone(), field_type: "text".into(), }], ..Default::default() }; let err = post_table_definition(&pool, req).await.unwrap_err(); assert_eq!(err.code(), Code::InvalidArgument); } // 15) Decimal precision overflow must be caught by our parser. #[rstest] #[tokio::test] async fn test_decimal_precision_overflow(#[future] pool: PgPool) { let pool = pool.await; let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: "dp_overflow".into(), columns: vec![ColumnDefinition { name: "amount".into(), field_type: "decimal(9999999999,1)".into(), }], ..Default::default() }; let err = post_table_definition(&pool, req).await.unwrap_err(); assert_eq!(err.code(), Code::InvalidArgument); assert!( err .message() .to_lowercase() .contains("invalid precision"), "{}", err.message() ); } // 16) Repeated profile insertion only creates one profile row. #[rstest] #[tokio::test] async fn test_repeated_profile_insertion(#[future] pool: PgPool) { let pool = pool.await; let prof = "repeat_prof"; post_table_definition( &pool, PostTableDefinitionRequest { profile_name: prof.into(), table_name: "t1".into(), ..Default::default() }, ) .await .unwrap(); post_table_definition( &pool, PostTableDefinitionRequest { profile_name: prof.into(), table_name: "t2".into(), ..Default::default() }, ) .await .unwrap(); let cnt: i64 = sqlx::query_scalar!( "SELECT COUNT(*) FROM schemas WHERE name = $1", // FIXED: Changed from profiles to schemas prof ) .fetch_one(&pool) .await .unwrap() .unwrap(); assert_eq!(cnt, 1); }