Files
komp_ac/server/tests/table_definition/post_table_definition_test2.rs
2025-06-18 21:37:30 +02:00

503 lines
14 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,
&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(),
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 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='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) 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 gen.\"selfref\"(id)"),
"{:?}",
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(),
..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) 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(),
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) 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::Internal, "{:?}", col);
}
}
// 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::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);
}