robust testing of the table definitions
This commit is contained in:
@@ -340,3 +340,108 @@ async fn test_database_error_on_closed_pool(
|
|||||||
// Assert
|
// Assert
|
||||||
assert_eq!(result.unwrap_err().code(), Code::Internal);
|
assert_eq!(result.unwrap_err().code(), Code::Internal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that minimal, uppercase and whitespace‐padded decimal specs
|
||||||
|
// are accepted and correctly mapped to NUMERIC(p, s).
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_valid_decimal_variants(#[future] pool: PgPool) {
|
||||||
|
let pool = pool.await;
|
||||||
|
let cases = vec![
|
||||||
|
("decimal(1,1)", "NUMERIC(1, 1)"),
|
||||||
|
("decimal(1,0)", "NUMERIC(1, 0)"),
|
||||||
|
("DECIMAL(5,2)", "NUMERIC(5, 2)"),
|
||||||
|
("decimal( 5 , 2 )", "NUMERIC(5, 2)"),
|
||||||
|
];
|
||||||
|
for (i, (typ, expect)) in cases.into_iter().enumerate() {
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: format!("dec_valid_{}", i),
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: "amount".into(),
|
||||||
|
field_type: typ.into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let resp = post_table_definition(&pool, request).await.unwrap();
|
||||||
|
assert!(resp.success, "{}", typ);
|
||||||
|
assert!(
|
||||||
|
resp.sql.contains(expect),
|
||||||
|
"expected `{}` to map to {}, got `{}`",
|
||||||
|
typ,
|
||||||
|
expect,
|
||||||
|
resp.sql
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that malformed decimal inputs are rejected with InvalidArgument.
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_on_malformed_decimal_inputs(#[future] pool: PgPool) {
|
||||||
|
let pool = pool.await;
|
||||||
|
let bad = vec!["decimal", "decimal()", "decimal(5,)", "decimal(,2)", "decimal(, )"];
|
||||||
|
for (i, typ) in bad.into_iter().enumerate() {
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: format!("dec_bad_{}", i),
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: "amt".into(),
|
||||||
|
field_type: typ.into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = post_table_definition(&pool, request).await.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::InvalidArgument, "{}", typ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that obviously invalid column identifiers are rejected
|
||||||
|
// (start with digit/underscore, contain space or hyphen, or are empty).
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_on_invalid_column_names(#[future] pool: PgPool) {
|
||||||
|
let pool = pool.await;
|
||||||
|
let bad_names = vec!["1col", "_col", "col name", "col-name", ""];
|
||||||
|
for name in bad_names {
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "tbl_invalid_cols".into(),
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: name.into(),
|
||||||
|
field_type: "text".into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = post_table_definition(&pool, request).await.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::InvalidArgument, "{}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that a user‐supplied column ending in "_id" is rejected
|
||||||
|
// to avoid collision with system‐generated FKs.
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_on_column_name_suffix_id(#[future] pool: PgPool) {
|
||||||
|
let pool = pool.await;
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "tbl_suffix_id".into(),
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: "user_id".into(),
|
||||||
|
field_type: "text".into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = post_table_definition(&pool, request).await.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::InvalidArgument);
|
||||||
|
assert!(
|
||||||
|
err.message().to_lowercase().contains("invalid column name"),
|
||||||
|
"unexpected error message: {}",
|
||||||
|
err.message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
include!("post_table_definition_test2.rs");
|
||||||
|
include!("post_table_definition_test3.rs");
|
||||||
|
include!("post_table_definition_test4.rs");
|
||||||
|
|||||||
502
server/tests/table_definition/post_table_definition_test2.rs
Normal file
502
server/tests/table_definition/post_table_definition_test2.rs
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
244
server/tests/table_definition/post_table_definition_test3.rs
Normal file
244
server/tests/table_definition/post_table_definition_test3.rs
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// tests/table_definition/post_table_definition_test3.rs
|
||||||
|
|
||||||
|
// NOTE: All 'use' statements have been removed from this file.
|
||||||
|
// They are inherited from the parent file that includes this one.
|
||||||
|
|
||||||
|
// ========= Helper Functions for this Test File =========
|
||||||
|
|
||||||
|
/// Checks that a table definition does NOT exist for a given profile and table name.
|
||||||
|
async fn assert_table_definition_does_not_exist(pool: &PgPool, profile_name: &str, table_name: &str) {
|
||||||
|
let count: i64 = sqlx::query_scalar!(
|
||||||
|
"SELECT COUNT(*) FROM table_definitions td
|
||||||
|
JOIN profiles p ON td.profile_id = p.id
|
||||||
|
WHERE p.name = $1 AND td.table_name = $2",
|
||||||
|
profile_name,
|
||||||
|
table_name
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to query for table definition")
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
count, 0,
|
||||||
|
"Table definition for '{}/{}' was found but should have been rolled back.",
|
||||||
|
profile_name, table_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= Category 2: Advanced Identifier and Naming Collisions =========
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_on_column_name_collision_with_fk(
|
||||||
|
#[future] pool_with_preexisting_table: PgPool,
|
||||||
|
) {
|
||||||
|
// Scenario: Create a table that links to 'customers' and also defines its own 'customers_id' column.
|
||||||
|
// Expected: The generated CREATE TABLE will have a duplicate column, causing a database error.
|
||||||
|
let pool = pool_with_preexisting_table.await; // Provides 'customers' table
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "orders_collision".into(),
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: "customers_id".into(), // This will collide with the generated FK
|
||||||
|
field_type: "integer".into(),
|
||||||
|
}],
|
||||||
|
links: vec![TableLink {
|
||||||
|
linked_table_name: "customers".into(),
|
||||||
|
required: true,
|
||||||
|
}],
|
||||||
|
indexes: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = post_table_definition(&pool, request).await;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.code(),
|
||||||
|
Code::Internal,
|
||||||
|
"Expected Internal error due to duplicate column in CREATE TABLE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_on_duplicate_column_names_in_request(#[future] pool: PgPool) {
|
||||||
|
// Scenario: The request itself contains two columns with the same name.
|
||||||
|
// Expected: Database error on CREATE TABLE with duplicate column definition.
|
||||||
|
let pool = pool.await;
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "duplicate_cols".into(),
|
||||||
|
columns: vec![
|
||||||
|
ColumnDefinition {
|
||||||
|
name: "product_name".into(),
|
||||||
|
field_type: "text".into(),
|
||||||
|
},
|
||||||
|
ColumnDefinition {
|
||||||
|
name: "product_name".into(),
|
||||||
|
field_type: "text".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = post_table_definition(&pool, request).await;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::Internal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_link_to_sanitized_table_name(#[future] pool: PgPool) {
|
||||||
|
// Scenario: Test that linking requires using the sanitized name, not the original.
|
||||||
|
let pool = pool.await;
|
||||||
|
let original_name = "My Invoices";
|
||||||
|
let sanitized_name = "myinvoices";
|
||||||
|
|
||||||
|
// 1. Create the table with a name that requires sanitization.
|
||||||
|
let create_req = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: original_name.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let resp = post_table_definition(&pool, create_req).await.unwrap();
|
||||||
|
assert!(resp.sql.contains(&format!("gen.\"{}\"", sanitized_name)));
|
||||||
|
|
||||||
|
// 2. Attempt to link to the *original* name, which should fail.
|
||||||
|
let link_req_fail = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "payments".into(),
|
||||||
|
links: vec![TableLink {
|
||||||
|
linked_table_name: original_name.into(),
|
||||||
|
required: true,
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = post_table_definition(&pool, link_req_fail)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::NotFound);
|
||||||
|
assert!(err.message().contains("Linked table My Invoices not found"));
|
||||||
|
|
||||||
|
// 3. Attempt to link to the *sanitized* name, which should succeed.
|
||||||
|
let link_req_success = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "payments_sanitized".into(),
|
||||||
|
links: vec![TableLink {
|
||||||
|
linked_table_name: sanitized_name.into(),
|
||||||
|
required: true,
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let success_resp = post_table_definition(&pool, link_req_success).await.unwrap();
|
||||||
|
assert!(success_resp.success);
|
||||||
|
assert!(success_resp
|
||||||
|
.sql
|
||||||
|
.contains(&format!("REFERENCES gen.\"{}\"(id)", sanitized_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= Category 3: Complex Link and Profile Logic =========
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_on_true_self_referential_link(#[future] pool: PgPool) {
|
||||||
|
// Scenario: A table attempts to link to itself in the same request.
|
||||||
|
// Expected: NotFound, because the table definition doesn't exist yet at link-check time.
|
||||||
|
let pool = pool.await;
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "employees".into(),
|
||||||
|
links: vec![TableLink {
|
||||||
|
linked_table_name: "employees".into(), // Self-reference
|
||||||
|
required: false, // For a manager_id FK
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = post_table_definition(&pool, request).await;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::NotFound);
|
||||||
|
assert!(err.message().contains("Linked table employees not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_behavior_on_empty_profile_name(#[future] pool: PgPool) {
|
||||||
|
// Scenario: Attempt to create a table with an empty profile name.
|
||||||
|
// Expected: This should violate the database's NOT NULL constraint on profiles.name.
|
||||||
|
let pool = pool.await;
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "".into(),
|
||||||
|
table_name: "table_in_empty_profile".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = post_table_definition(&pool, request).await;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.code(),
|
||||||
|
Code::Internal,
|
||||||
|
"Expected Internal error from DB constraint violation"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
err.message().to_lowercase().contains("profile error"),
|
||||||
|
"Unexpected error message: {}",
|
||||||
|
err.message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= Category 4: Concurrency =========
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "Concurrency tests can be flaky and require careful setup"]
|
||||||
|
async fn test_race_condition_on_table_creation(#[future] pool: PgPool) {
|
||||||
|
// Scenario: Two requests try to create the exact same table at the same time.
|
||||||
|
// Expected: One succeeds, the other fails with AlreadyExists.
|
||||||
|
let pool = pool.await;
|
||||||
|
let request1 = PostTableDefinitionRequest {
|
||||||
|
profile_name: "concurrent_profile".into(),
|
||||||
|
table_name: "racy_table".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let request2 = request1.clone();
|
||||||
|
|
||||||
|
let pool1 = pool.clone();
|
||||||
|
let pool2 = pool.clone();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let (res1, res2) = tokio::join!(
|
||||||
|
post_table_definition(&pool1, request1),
|
||||||
|
post_table_definition(&pool2, request2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let results = vec![res1, res2];
|
||||||
|
let success_count = results.iter().filter(|r| r.is_ok()).count();
|
||||||
|
let failure_count = results.iter().filter(|r| r.is_err()).count();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
success_count, 1,
|
||||||
|
"Exactly one request should succeed"
|
||||||
|
);
|
||||||
|
assert_eq!(failure_count, 1, "Exactly one request should fail");
|
||||||
|
|
||||||
|
let err = results
|
||||||
|
.into_iter()
|
||||||
|
.find(|r| r.is_err())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::AlreadyExists);
|
||||||
|
assert_eq!(err.message(), "Table already exists in this profile");
|
||||||
|
}
|
||||||
192
server/tests/table_definition/post_table_definition_test4.rs
Normal file
192
server/tests/table_definition/post_table_definition_test4.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// tests/table_definition/post_table_definition_test4.rs
|
||||||
|
|
||||||
|
// NOTE: All 'use' statements are inherited from the parent file that includes this one.
|
||||||
|
|
||||||
|
// ========= Category 5: Implementation-Specific Edge Cases =========
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_column_name_with_id_suffix_is_allowed(#[future] pool: PgPool) {
|
||||||
|
// NOTE: This test confirms the CURRENT behavior of the code, which is that creating
|
||||||
|
// a column with an `_id` suffix is allowed. The existing test
|
||||||
|
// `test_fail_on_column_name_suffix_id` makes a contrary assumption.
|
||||||
|
// If this behavior is undesirable, the `is_valid_identifier` function or its
|
||||||
|
// usage in the handler should be updated to reject such names.
|
||||||
|
let pool = pool.await;
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "orders_with_custom_id".into(),
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: "legacy_order_id".into(), // A user-defined column ending in `_id`
|
||||||
|
field_type: "integer".into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = post_table_definition(&pool, request).await.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert!(response.success);
|
||||||
|
assert!(response.sql.contains("\"legacy_order_id\" INTEGER"));
|
||||||
|
|
||||||
|
// Verify in the actual database
|
||||||
|
assert_table_structure_is_correct(
|
||||||
|
&pool,
|
||||||
|
"orders_with_custom_id",
|
||||||
|
&[("legacy_order_id", "integer")],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_on_fk_base_name_collision(#[future] pool: PgPool) {
|
||||||
|
// Scenario: Link to two tables (`team1_users`, `team2_users`) that both have a
|
||||||
|
// base name of "users". This should cause a duplicate "users_id" column in the
|
||||||
|
// generated SQL.
|
||||||
|
let pool = pool.await;
|
||||||
|
|
||||||
|
// Arrange: Create the two prerequisite tables
|
||||||
|
let req1 = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "team1_users".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
post_table_definition(&pool, req1).await.unwrap();
|
||||||
|
|
||||||
|
let req2 = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "team2_users".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
post_table_definition(&pool, req2).await.unwrap();
|
||||||
|
|
||||||
|
// Arrange: A request that links to both, causing the collision
|
||||||
|
let colliding_req = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "tasks".into(),
|
||||||
|
links: vec![
|
||||||
|
TableLink {
|
||||||
|
linked_table_name: "team1_users".into(),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
TableLink {
|
||||||
|
linked_table_name: "team2_users".into(),
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = post_table_definition(&pool, colliding_req).await;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.code(),
|
||||||
|
Code::Internal,
|
||||||
|
"Expected Internal error from duplicate column in CREATE TABLE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sql_reserved_keywords_as_identifiers_are_allowed(#[future] pool: PgPool) {
|
||||||
|
// NOTE: This test confirms that the system currently allows SQL reserved keywords
|
||||||
|
// as column names because they are correctly quoted. This is technically correct,
|
||||||
|
// but some systems add validation to block this as a policy to prevent user confusion.
|
||||||
|
let pool = pool.await;
|
||||||
|
let keywords = vec!["user", "select", "group", "order"];
|
||||||
|
|
||||||
|
for (i, keyword) in keywords.into_iter().enumerate() {
|
||||||
|
let table_name = format!("keyword_test_{}", i);
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: table_name.clone(),
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: keyword.into(),
|
||||||
|
field_type: "text".into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
let response = post_table_definition(&pool, request)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to create table with reserved keyword '{}': {:?}",
|
||||||
|
keyword, e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(response.success);
|
||||||
|
assert!(response.sql.contains(&format!("\"{}\" TEXT", keyword)));
|
||||||
|
|
||||||
|
assert_table_structure_is_correct(&pool, &table_name, &[(keyword, "text")]).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= Category 6: Environmental and Extreme Edge Cases =========
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sanitization_of_unicode_and_special_chars(#[future] pool: PgPool) {
|
||||||
|
// Scenario: Use identifiers with characters that should be stripped by sanitization,
|
||||||
|
// including multi-byte unicode (emoji) and a null byte.
|
||||||
|
let pool = pool.await;
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "produits_😂".into(), // Should become "produits_"
|
||||||
|
columns: vec![ColumnDefinition {
|
||||||
|
name: "col\0with_null".into(), // Should become "colwith_null"
|
||||||
|
field_type: "text".into(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = post_table_definition(&pool, request).await.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert!(response.success);
|
||||||
|
|
||||||
|
// Assert that the generated SQL contains the SANITIZED names
|
||||||
|
assert!(response.sql.contains("CREATE TABLE gen.\"produits_\""));
|
||||||
|
assert!(response.sql.contains("\"colwith_null\" TEXT"));
|
||||||
|
|
||||||
|
// Verify the actual structure in the database
|
||||||
|
assert_table_structure_is_correct(&pool, "produits_", &[("colwith_null", "text")]).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_fail_gracefully_if_schema_is_missing(#[future] pool: PgPool) {
|
||||||
|
// Scenario: The handler relies on the 'gen' schema existing. This test ensures
|
||||||
|
// it fails gracefully if that assumption is broken.
|
||||||
|
let pool = pool.await;
|
||||||
|
|
||||||
|
// Arrange: Drop the schema that the handler needs
|
||||||
|
sqlx::query("DROP SCHEMA gen CASCADE;")
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to drop 'gen' schema for test setup");
|
||||||
|
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
profile_name: "default".into(),
|
||||||
|
table_name: "this_will_fail".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = post_table_definition(&pool, request).await;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert_eq!(err.code(), Code::Internal);
|
||||||
|
// Check for the Postgres error message for a missing schema.
|
||||||
|
assert!(err.message().to_lowercase().contains("schema \"gen\" does not exist"));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user