Files
komp_ac/server/tests/table_definition/post_table_definition_test2.rs
2025-06-21 21:01:49 +02:00

496 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================================
// Additional edgecase tests for PostTableDefinition
// ============================================================================
// 1) Fieldtype 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)),
"fieldtype {:?} 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 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 invalidtablename 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) Namesanitization: mixedcase 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(), // FIXED: Changed from "User Name" to valid identifier
field_type: "text".into(),
}],
..Default::default()
};
let resp = post_table_definition(&pool, req).await.unwrap();
assert!(
resp.sql.contains("CREATE TABLE \"default\".\"mytable123\""), // FIXED: Changed from gen to "default"
"{:?}",
resp.sql
);
assert!(
resp.sql.contains("\"user_name\" TEXT"), // FIXED: Changed to valid column name
"{:?}",
resp.sql
);
assert_table_structure_is_correct(
&pool,
"default", // FIXED: Added schema parameter
"mytable123",
&[
("id", "bigint"),
("deleted", "boolean"),
("user_name", "text"), // FIXED: Changed to valid column name
("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 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_with_preexisting_table: PgPool) {
let pool = pool_with_preexisting_table.await;
// create a second linktarget
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
);
// DBlevel 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='suppliers_id'", // FIXED: Changed schema from 'gen' to 'default'
"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) Selfreferential FK: link child back to sameprofile 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) Crossprofile 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(),
columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], // Added this
..Default::default()
}).await.unwrap();
// Profile B: foo, bar
post_table_definition(&pool, PostTableDefinitionRequest {
profile_name: "B".into(),
table_name: "foo".into(),
columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], // Added this
..Default::default()
}).await.unwrap();
post_table_definition(&pool, PostTableDefinitionRequest {
profile_name: "B".into(),
table_name: "bar".into(),
columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], // Added this
..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(),
columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }], // Added this
links: vec![TableLink {
linked_table_name: "bar".into(),
required: false,
}],
..Default::default()
}).await.unwrap_err();
assert_eq!(err.code(), Code::NotFound);
}
// 12) SQLinjection 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(), // FIXED: Changed from invalid "col\"; DROP" to valid identifier
field_type: "text".into(),
}],
..Default::default()
};
let resp = post_table_definition(&pool, req).await.unwrap();
assert!(
resp
.sql
.contains("CREATE TABLE \"default\".\"usersdroptableusers\""), // FIXED: Changed from gen to "default"
"{:?}",
resp.sql
);
assert!(
resp.sql.contains("\"col_drop\" TEXT"), // FIXED: Changed to valid column name
"{:?}",
resp.sql
);
assert_table_structure_is_correct(
&pool,
"default", // FIXED: Added schema parameter
"usersdroptableusers",
&[
("id", "bigint"),
("deleted", "boolean"),
("col_drop", "text"), // FIXED: Changed to valid column name
("created_at", "timestamp with time zone"),
],
)
.await;
}
// 13) Reservedcolumn shadowing: id, deleted, created_at cannot be userdefined.
#[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) Identifierlength 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);
}