// ============================================================================ // 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, &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 bad_idxs = vec!["1col", "_col", "col-name"]; for idx in bad_idxs { 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 err = post_table_definition(&pool, req).await.unwrap_err(); assert_eq!(err.code(), Code::InvalidArgument); assert!( err .message() .to_lowercase() .contains("invalid index name"), "{:?} yielded wrong message: {}", idx, err.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"), ("!@#$", "cannot be empty"), ("__", "cannot be empty"), ]; for (name, expected_msg) in cases { let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: name.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(expected_msg), "{:?} => {}", name, err.message() ); } } // 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(), columns: vec![ColumnDefinition { name: "User Name".into(), field_type: "text".into(), }], ..Default::default() }; let resp = post_table_definition(&pool, req).await.unwrap(); assert!( resp.sql.contains("CREATE TABLE gen.\"mytable123\""), "{:?}", resp.sql ); assert!( resp.sql.contains("\"username\" TEXT"), "{:?}", resp.sql ); assert_table_structure_is_correct( &pool, "mytable123", &[ ("id", "bigint"), ("deleted", "boolean"), ("username", "text"), ("created_at", "timestamp with time zone"), ], ) .await; } // 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 req = PostTableDefinitionRequest { profile_name: "default".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, "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_with_preexisting_table: PgPool) { let pool = pool_with_preexisting_table.await; // create a second link‐target let sup = PostTableDefinitionRequest { profile_name: "default".into(), table_name: "suppliers".into(), columns: vec![ColumnDefinition { name: "sup_name".into(), field_type: "text".into(), }], indexes: vec!["sup_name".into()], links: vec![], }; post_table_definition(&pool, sup).await.unwrap(); let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: "orders_links".into(), columns: vec![], indexes: vec![], links: vec![ TableLink { linked_table_name: "customers".into(), required: true, }, TableLink { linked_table_name: "suppliers".into(), required: false, }, ], }; let resp = post_table_definition(&pool, req).await.unwrap(); assert!( resp .sql .contains("\"customers_id\" BIGINT NOT NULL"), "{:?}", resp.sql ); assert!( resp.sql.contains("\"suppliers_id\" BIGINT"), "{:?}", resp.sql ); // DB‐level nullability for optional FK let is_nullable: String = sqlx::query_scalar!( "SELECT is_nullable \ FROM information_schema.columns \ WHERE table_schema='gen' \ AND table_name=$1 \ AND column_name='suppliers_id'", "orders_links" ) .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_with_preexisting_table: PgPool) { let pool = pool_with_preexisting_table.await; let req = PostTableDefinitionRequest { profile_name: "default".into(), table_name: "dup_links".into(), columns: vec![], indexes: vec![], links: vec![ TableLink { linked_table_name: "customers".into(), required: true, }, TableLink { linked_table_name: "customers".into(), required: false, }, ], }; let err = post_table_definition(&pool, req).await.unwrap_err(); assert_eq!(err.code(), Code::Internal); } // 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 gen.\"selfref\"(id)"), "{:?}", 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 post_table_definition( &pool, PostTableDefinitionRequest { profile_name: "A".into(), table_name: "foo".into(), ..Default::default() }, ) .await .unwrap(); // Profile B: foo, bar post_table_definition( &pool, PostTableDefinitionRequest { profile_name: "B".into(), table_name: "foo".into(), ..Default::default() }, ) .await .unwrap(); post_table_definition( &pool, PostTableDefinitionRequest { profile_name: "B".into(), table_name: "bar".into(), ..Default::default() }, ) .await .unwrap(); // A linking to B.bar → NotFound let err = post_table_definition( &pool, PostTableDefinitionRequest { profile_name: "A".into(), table_name: "linker".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(), columns: vec![ColumnDefinition { name: "col\"; DROP".into(), field_type: "text".into(), }], ..Default::default() }; let resp = post_table_definition(&pool, req).await.unwrap(); assert!( resp .sql .contains("CREATE TABLE gen.\"usersdroptableusers\""), "{:?}", resp.sql ); assert!( resp.sql.contains("\"coldrop\" TEXT"), "{:?}", resp.sql ); assert_table_structure_is_correct( &pool, "usersdroptableusers", &[ ("id", "bigint"), ("deleted", "boolean"), ("coldrop", "text"), ("created_at", "timestamp with time zone"), ], ) .await; } // 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::Internal, "{:?}", col); } } // 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::Internal); } // 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 profiles WHERE name = $1", prof ) .fetch_one(&pool) .await .unwrap() .unwrap(); assert_eq!(cnt, 1); }