mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
test: add comprehensive test coverage for extracted backend crates
Add 296 tests across unit and integration test suites to cover the newly extracted crates from the recent refactor commits. Unit tests (270): - windmill-trigger-postgres (96): hex codec, bool parsing, type conversion, relation tracking, replication message parsing, publication data validation - windmill-trigger-http (92): HMAC signature verification for GitHub/Slack/Stripe/TikTok/Twitch/Zoom webhooks, API key auth, Basic Auth, route validation, HTTP method/request type serde - windmill-api-jobs (39): SQL query builder for job listing/counting with filters, pagination, label handling - windmill-trigger (31): TriggerMode serde, query pagination, BaseTriggerData backward compat, HandlerAction, ServerState - windmill-common webhook (7): WebhookMessage serialization tags - worker nativets/postgresql (5): nativets job execution with args/objects/datetime, postgresql query execution Integration tests (26): - backend/tests/triggers.rs: capture config CRUD, capture payload operations, capture API endpoints, HTTP trigger CRUD with mode filtering, all trigger types DB schema validation (websocket, kafka, postgres, nats, sqs), schedule operations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1145,6 +1145,195 @@ public class Main {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_nativets_job(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
export async function main(name: string): Promise<string> {
|
||||
return `hello ${name}`;
|
||||
}
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let result = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Nativets,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}))
|
||||
.arg("name", json!("world"))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!("hello world"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_nativets_job_with_args(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
export async function main(a: number, b: number): Promise<number> {
|
||||
return a + b;
|
||||
}
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let result = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Nativets,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}))
|
||||
.arg("a", json!(3))
|
||||
.arg("b", json!(7))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!(10));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_nativets_job_object_return(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
export async function main(items: string[]): Promise<{ count: number; items: string[] }> {
|
||||
return { count: items.length, items };
|
||||
}
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let result = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Nativets,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}))
|
||||
.arg("items", json!(["a", "b", "c"]))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, json!({"count": 3, "items": ["a", "b", "c"]}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_nativets_job_datetime(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// nativets passes Date-typed args as strings (no auto-conversion unlike Bun/Deno)
|
||||
let content = r#"
|
||||
export async function main(a: Date) {
|
||||
return typeof a;
|
||||
}
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let result = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Nativets,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}))
|
||||
.arg("a", json!("2024-09-24T10:00:00.000Z"))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!("string"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_postgresql_job(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
-- $1 name
|
||||
SELECT 'hello ' || $1::text AS result;
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let result = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Postgresql,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}))
|
||||
.arg("name", json!("world"))
|
||||
.arg(
|
||||
"database",
|
||||
json!({"host": "localhost", "port": 5432, "dbname": "windmill", "user": "postgres", "password": "changeme"}),
|
||||
)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, json!([{"result": "hello world"}]));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_bun_job_datetime(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
@@ -371,3 +371,441 @@ pub fn list_completed_jobs_query(
|
||||
|
||||
filter_list_completed_query(sqlb, lq, w_id, join_outstanding_wait_times)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn empty_queue_query() -> ListQueueQuery {
|
||||
ListQueueQuery {
|
||||
script_path_start: None,
|
||||
script_path_exact: None,
|
||||
script_hash: None,
|
||||
created_by: None,
|
||||
started_before: None,
|
||||
started_after: None,
|
||||
created_before: None,
|
||||
created_after: None,
|
||||
created_or_started_before: None,
|
||||
created_or_started_after: None,
|
||||
running: None,
|
||||
parent_job: None,
|
||||
order_desc: None,
|
||||
job_kinds: None,
|
||||
suspended: None,
|
||||
args: None,
|
||||
tag: None,
|
||||
schedule_path: None,
|
||||
scheduled_for_before_now: None,
|
||||
all_workspaces: None,
|
||||
is_flow_step: None,
|
||||
has_null_parent: None,
|
||||
is_not_schedule: None,
|
||||
concurrency_key: None,
|
||||
worker: None,
|
||||
allow_wildcards: None,
|
||||
trigger_kind: None,
|
||||
trigger_path: None,
|
||||
include_args: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_completed_query() -> ListCompletedQuery {
|
||||
ListCompletedQuery {
|
||||
script_path_start: None,
|
||||
script_path_exact: None,
|
||||
script_hash: None,
|
||||
created_by: None,
|
||||
started_before: None,
|
||||
started_after: None,
|
||||
created_before: None,
|
||||
created_after: None,
|
||||
created_or_started_before: None,
|
||||
created_or_started_after: None,
|
||||
created_or_started_after_completed_jobs: None,
|
||||
created_before_queue: None,
|
||||
created_after_queue: None,
|
||||
completed_after: None,
|
||||
completed_before: None,
|
||||
success: None,
|
||||
running: None,
|
||||
parent_job: None,
|
||||
order_desc: None,
|
||||
job_kinds: None,
|
||||
is_skipped: None,
|
||||
is_flow_step: None,
|
||||
suspended: None,
|
||||
schedule_path: None,
|
||||
args: None,
|
||||
result: None,
|
||||
tag: None,
|
||||
scheduled_for_before_now: None,
|
||||
all_workspaces: None,
|
||||
has_null_parent: None,
|
||||
label: None,
|
||||
is_not_schedule: None,
|
||||
concurrency_key: None,
|
||||
worker: None,
|
||||
allow_wildcards: None,
|
||||
trigger_kind: None,
|
||||
trigger_path: None,
|
||||
include_args: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sql(sqlb: SqlBuilder) -> String {
|
||||
sqlb.sql().unwrap_or_default()
|
||||
}
|
||||
|
||||
// --- Queue query tests ---
|
||||
|
||||
#[test]
|
||||
fn test_queue_basic_query() {
|
||||
let lq = empty_queue_query();
|
||||
let sqlb = list_queue_jobs_query(
|
||||
"test_ws",
|
||||
&lq,
|
||||
&["v2_job_queue.id"],
|
||||
Pagination { page: Some(1), per_page: Some(10) },
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("v2_job_queue"));
|
||||
assert!(sql.contains("v2_job_queue.workspace_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_script_path_start() {
|
||||
let lq = ListQueueQuery {
|
||||
script_path_start: Some("f/test".to_string()),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("runnable_path"));
|
||||
assert!(sql.contains("LIKE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_script_path_exact() {
|
||||
let lq = ListQueueQuery {
|
||||
script_path_exact: Some("f/test/script".to_string()),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("runnable_path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_running() {
|
||||
let lq = ListQueueQuery {
|
||||
running: Some(true),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("running"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_job_kinds() {
|
||||
let lq = ListQueueQuery {
|
||||
job_kinds: Some("script,flow".to_string()),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("kind"));
|
||||
assert!(sql.contains("IN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_suspended() {
|
||||
let lq = ListQueueQuery {
|
||||
suspended: Some(true),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("suspend"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_is_not_schedule() {
|
||||
let lq = ListQueueQuery {
|
||||
is_not_schedule: Some(true),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("trigger_kind IS DISTINCT FROM"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_has_null_parent() {
|
||||
let lq = ListQueueQuery {
|
||||
has_null_parent: Some(true),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("parent_job IS NULL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_is_flow_step_true() {
|
||||
let lq = ListQueueQuery {
|
||||
is_flow_step: Some(true),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("flow_step_id IS NOT NULL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_filter_is_flow_step_false() {
|
||||
let lq = ListQueueQuery {
|
||||
is_flow_step: Some(false),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("flow_step_id IS NULL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_admins_all_workspaces() {
|
||||
let lq = ListQueueQuery {
|
||||
all_workspaces: Some(true),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"admins",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(!sql.contains("workspace_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_non_admins_ignores_all_workspaces() {
|
||||
let lq = ListQueueQuery {
|
||||
all_workspaces: Some(true),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"other_ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("workspace_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_with_outstanding_wait_times() {
|
||||
let lq = empty_queue_query();
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
true,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("outstanding_wait_time"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queue_schedule_path_filter() {
|
||||
let lq = ListQueueQuery {
|
||||
schedule_path: Some("f/test/schedule".to_string()),
|
||||
..empty_queue_query()
|
||||
};
|
||||
let sqlb = filter_list_queue_query(
|
||||
SqlBuilder::select_from("v2_job_queue").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("trigger"));
|
||||
assert!(sql.contains("'schedule'"));
|
||||
}
|
||||
|
||||
// --- Completed query tests ---
|
||||
|
||||
#[test]
|
||||
fn test_completed_basic_query() {
|
||||
let lq = empty_completed_query();
|
||||
let sqlb = list_completed_jobs_query("test_ws", Some(10), 0, &lq, &["id"], false, None);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("v2_job_completed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_filter_success_true() {
|
||||
let lq = ListCompletedQuery {
|
||||
success: Some(true),
|
||||
..empty_completed_query()
|
||||
};
|
||||
let sqlb = filter_list_completed_query(
|
||||
SqlBuilder::select_from("v2_job_completed").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("'success'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_filter_success_false() {
|
||||
let lq = ListCompletedQuery {
|
||||
success: Some(false),
|
||||
..empty_completed_query()
|
||||
};
|
||||
let sqlb = filter_list_completed_query(
|
||||
SqlBuilder::select_from("v2_job_completed").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("'failure'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_order_by_completed_at() {
|
||||
let lq = ListCompletedQuery {
|
||||
completed_after: Some(chrono::Utc::now()),
|
||||
..empty_completed_query()
|
||||
};
|
||||
let sqlb = list_completed_jobs_query("ws", Some(10), 0, &lq, &["id"], false, None);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("completed_at"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_filter_label() {
|
||||
let lq = ListCompletedQuery {
|
||||
label: Some("deploy".to_string()),
|
||||
..empty_completed_query()
|
||||
};
|
||||
let sqlb = filter_list_completed_query(
|
||||
SqlBuilder::select_from("v2_job_completed").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("wm_labels"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_filter_is_skipped() {
|
||||
let lq = ListCompletedQuery {
|
||||
is_skipped: Some(true),
|
||||
..empty_completed_query()
|
||||
};
|
||||
let sqlb = filter_list_completed_query(
|
||||
SqlBuilder::select_from("v2_job_completed").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("'skipped'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_with_tags() {
|
||||
let lq = empty_completed_query();
|
||||
let sqlb = list_completed_jobs_query(
|
||||
"ws",
|
||||
Some(10),
|
||||
0,
|
||||
&lq,
|
||||
&["id"],
|
||||
false,
|
||||
Some(vec!["tag1", "tag2"]),
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("v2_job.tag"));
|
||||
assert!(sql.contains("IN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_no_limit() {
|
||||
let lq = empty_completed_query();
|
||||
let sqlb = list_completed_jobs_query("ws", None, 0, &lq, &["id"], false, None);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(!sql.contains("LIMIT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_result_filter() {
|
||||
let lq = ListCompletedQuery {
|
||||
result: Some(r#"{"status": "ok"}"#.to_string()),
|
||||
..empty_completed_query()
|
||||
};
|
||||
let sqlb = filter_list_completed_query(
|
||||
SqlBuilder::select_from("v2_job_completed").clone(),
|
||||
&lq,
|
||||
"ws",
|
||||
false,
|
||||
);
|
||||
let sql = build_sql(sqlb);
|
||||
assert!(sql.contains("result @>"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,3 +568,279 @@ pub fn add_raw_string(
|
||||
|
||||
use anyhow::Context;
|
||||
use base64::Engine;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
// --- decode_payload ---
|
||||
|
||||
#[test]
|
||||
fn test_decode_payload_valid() {
|
||||
let payload = base64::engine::general_purpose::STANDARD
|
||||
.encode(r#"{"key": "value"}"#);
|
||||
let result: HashMap<String, serde_json::Value> = decode_payload(payload).unwrap();
|
||||
assert_eq!(result["key"], json!("value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_payload_invalid_base64() {
|
||||
let result: anyhow::Result<HashMap<String, serde_json::Value>> =
|
||||
decode_payload("not-valid-base64!!!".to_string());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_payload_invalid_json() {
|
||||
let payload = base64::engine::general_purpose::STANDARD.encode("not json");
|
||||
let result: anyhow::Result<HashMap<String, serde_json::Value>> = decode_payload(payload);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_payload_empty_object() {
|
||||
let payload = base64::engine::general_purpose::STANDARD.encode("{}");
|
||||
let result: HashMap<String, serde_json::Value> = decode_payload(payload).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
// --- add_raw_string ---
|
||||
|
||||
#[test]
|
||||
fn test_add_raw_string_some() {
|
||||
let args = serde_json::Map::new();
|
||||
let result = add_raw_string(Some("body content".to_string()), args);
|
||||
assert_eq!(
|
||||
result["raw_string"],
|
||||
serde_json::Value::String("body content".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_raw_string_none() {
|
||||
let args = serde_json::Map::new();
|
||||
let result = add_raw_string(None, args);
|
||||
assert!(!result.contains_key("raw_string"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_raw_string_preserves_existing() {
|
||||
let mut args = serde_json::Map::new();
|
||||
args.insert("existing".to_string(), json!("value"));
|
||||
let result = add_raw_string(Some("body".to_string()), args);
|
||||
assert_eq!(result["existing"], json!("value"));
|
||||
assert_eq!(result["raw_string"], json!("body"));
|
||||
}
|
||||
|
||||
// --- RunJobQuery ---
|
||||
|
||||
#[test]
|
||||
fn test_run_job_query_payload_as_args_none() {
|
||||
let q = RunJobQuery::default();
|
||||
let result = q.payload_as_args().unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_job_query_payload_as_args_valid() {
|
||||
let encoded = base64::engine::general_purpose::STANDARD
|
||||
.encode(r#"{"x": 42}"#);
|
||||
let q = RunJobQuery {
|
||||
payload: Some(encoded),
|
||||
..Default::default()
|
||||
};
|
||||
let result = q.payload_as_args().unwrap();
|
||||
assert!(result.contains_key("x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_job_query_payload_as_args_invalid() {
|
||||
let q = RunJobQuery {
|
||||
payload: Some("invalid!!!".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(q.payload_as_args().is_err());
|
||||
}
|
||||
|
||||
// --- ListCompletedQuery -> ListQueueQuery conversion ---
|
||||
|
||||
#[test]
|
||||
fn test_list_completed_to_queue_query_conversion() {
|
||||
let lcq = ListCompletedQuery {
|
||||
script_path_start: Some("f/test".to_string()),
|
||||
script_path_exact: None,
|
||||
script_hash: None,
|
||||
created_by: Some("admin".to_string()),
|
||||
started_before: None,
|
||||
started_after: None,
|
||||
created_before: Some(chrono::Utc::now()),
|
||||
created_after: None,
|
||||
created_or_started_before: None,
|
||||
created_or_started_after: None,
|
||||
created_or_started_after_completed_jobs: None,
|
||||
created_before_queue: None,
|
||||
created_after_queue: None,
|
||||
completed_after: None,
|
||||
completed_before: None,
|
||||
success: None,
|
||||
running: Some(true),
|
||||
parent_job: None,
|
||||
order_desc: Some(true),
|
||||
job_kinds: Some("script,flow".to_string()),
|
||||
is_skipped: None,
|
||||
is_flow_step: None,
|
||||
suspended: None,
|
||||
schedule_path: None,
|
||||
args: None,
|
||||
result: None,
|
||||
tag: Some("custom".to_string()),
|
||||
scheduled_for_before_now: None,
|
||||
all_workspaces: None,
|
||||
has_null_parent: None,
|
||||
label: None,
|
||||
is_not_schedule: None,
|
||||
concurrency_key: None,
|
||||
worker: None,
|
||||
allow_wildcards: None,
|
||||
trigger_kind: None,
|
||||
trigger_path: None,
|
||||
include_args: None,
|
||||
};
|
||||
|
||||
let lqq: ListQueueQuery = lcq.into();
|
||||
assert_eq!(lqq.script_path_start, Some("f/test".to_string()));
|
||||
assert_eq!(lqq.created_by, Some("admin".to_string()));
|
||||
assert_eq!(lqq.running, Some(true));
|
||||
assert_eq!(lqq.job_kinds, Some("script,flow".to_string()));
|
||||
assert_eq!(lqq.tag, Some("custom".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_completed_to_queue_prefers_queue_created_fields() {
|
||||
let specific_time = chrono::Utc::now();
|
||||
let other_time = specific_time - chrono::Duration::hours(1);
|
||||
|
||||
let lcq = ListCompletedQuery {
|
||||
script_path_start: None,
|
||||
script_path_exact: None,
|
||||
script_hash: None,
|
||||
created_by: None,
|
||||
started_before: None,
|
||||
started_after: None,
|
||||
created_before: Some(other_time),
|
||||
created_after: Some(other_time),
|
||||
created_or_started_before: None,
|
||||
created_or_started_after: None,
|
||||
created_or_started_after_completed_jobs: None,
|
||||
created_before_queue: Some(specific_time),
|
||||
created_after_queue: Some(specific_time),
|
||||
completed_after: None,
|
||||
completed_before: None,
|
||||
success: None,
|
||||
running: None,
|
||||
parent_job: None,
|
||||
order_desc: None,
|
||||
job_kinds: None,
|
||||
is_skipped: None,
|
||||
is_flow_step: None,
|
||||
suspended: None,
|
||||
schedule_path: None,
|
||||
args: None,
|
||||
result: None,
|
||||
tag: None,
|
||||
scheduled_for_before_now: None,
|
||||
all_workspaces: None,
|
||||
has_null_parent: None,
|
||||
label: None,
|
||||
is_not_schedule: None,
|
||||
concurrency_key: None,
|
||||
worker: None,
|
||||
allow_wildcards: None,
|
||||
trigger_kind: None,
|
||||
trigger_path: None,
|
||||
include_args: None,
|
||||
};
|
||||
|
||||
let lqq: ListQueueQuery = lcq.into();
|
||||
assert_eq!(lqq.created_before, Some(specific_time));
|
||||
assert_eq!(lqq.created_after, Some(specific_time));
|
||||
}
|
||||
|
||||
// --- UnifiedJob field constants ---
|
||||
|
||||
#[test]
|
||||
fn test_completed_job_fields_not_empty() {
|
||||
let fields = UnifiedJob::completed_job_fields();
|
||||
assert!(!fields.is_empty());
|
||||
assert!(fields.iter().any(|f| f.contains("typ")));
|
||||
assert!(fields.iter().any(|f| f.contains("workspace_id")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queued_job_fields_not_empty() {
|
||||
let fields = UnifiedJob::queued_job_fields();
|
||||
assert!(!fields.is_empty());
|
||||
assert!(fields.iter().any(|f| f.contains("typ")));
|
||||
assert!(fields.iter().any(|f| f.contains("scheduled_for")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_completed_and_queued_fields_same_count() {
|
||||
assert_eq!(
|
||||
UnifiedJob::completed_job_fields().len(),
|
||||
UnifiedJob::queued_job_fields().len(),
|
||||
"CJ and QJ field lists must have the same number of columns for UNION queries"
|
||||
);
|
||||
}
|
||||
|
||||
// --- ListableCompletedJob serialization ---
|
||||
|
||||
#[test]
|
||||
fn test_listable_completed_job_skip_none() {
|
||||
let job = ListableCompletedJob {
|
||||
r#type: "CompletedJob".to_string(),
|
||||
workspace_id: "test".to_string(),
|
||||
id: Uuid::nil(),
|
||||
parent_job: None,
|
||||
created_by: "admin".to_string(),
|
||||
created_at: chrono::Utc::now(),
|
||||
started_at: None,
|
||||
duration_ms: 100,
|
||||
success: true,
|
||||
script_hash: None,
|
||||
script_path: None,
|
||||
deleted: false,
|
||||
raw_code: None,
|
||||
canceled: false,
|
||||
canceled_by: None,
|
||||
canceled_reason: None,
|
||||
job_kind: JobKind::Script,
|
||||
schedule_path: None,
|
||||
permissioned_as: "u/admin".to_string(),
|
||||
flow_status: None,
|
||||
raw_flow: None,
|
||||
is_flow_step: false,
|
||||
language: None,
|
||||
is_skipped: false,
|
||||
email: "admin@test.com".to_string(),
|
||||
visible_to_owner: true,
|
||||
mem_peak: None,
|
||||
tag: "default".to_string(),
|
||||
priority: None,
|
||||
labels: None,
|
||||
args: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&job).unwrap();
|
||||
let obj = json.as_object().unwrap();
|
||||
assert!(!obj.contains_key("parent_job"));
|
||||
assert!(!obj.contains_key("script_hash"));
|
||||
assert!(!obj.contains_key("raw_code"));
|
||||
assert!(!obj.contains_key("canceled_by"));
|
||||
assert!(!obj.contains_key("mem_peak"));
|
||||
assert!(!obj.contains_key("labels"));
|
||||
assert!(obj.contains_key("type"));
|
||||
assert!(obj.contains_key("workspace_id"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,3 +152,125 @@ impl WebhookShared {
|
||||
let _ = self.channel.send(WebhookPayload::InstanceEvent(event));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_webhook_message_create_script() {
|
||||
let msg = WebhookMessage::CreateScript {
|
||||
workspace: "demo".to_string(),
|
||||
path: "f/test/script".to_string(),
|
||||
hash: "abc123".to_string(),
|
||||
};
|
||||
let json = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(json["type"], "CreateScript");
|
||||
assert_eq!(json["workspace"], "demo");
|
||||
assert_eq!(json["path"], "f/test/script");
|
||||
assert_eq!(json["hash"], "abc123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webhook_message_update_flow() {
|
||||
let msg = WebhookMessage::UpdateFlow {
|
||||
workspace: "staging".to_string(),
|
||||
old_path: "f/old/flow".to_string(),
|
||||
new_path: "f/new/flow".to_string(),
|
||||
};
|
||||
let json = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(json["type"], "UpdateFlow");
|
||||
assert_eq!(json["old_path"], "f/old/flow");
|
||||
assert_eq!(json["new_path"], "f/new/flow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webhook_message_delete_resource() {
|
||||
let msg = WebhookMessage::DeleteResource {
|
||||
workspace: "prod".to_string(),
|
||||
path: "u/admin/db".to_string(),
|
||||
};
|
||||
let json = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(json["type"], "DeleteResource");
|
||||
assert_eq!(json["workspace"], "prod");
|
||||
assert_eq!(json["path"], "u/admin/db");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webhook_message_create_folder() {
|
||||
let msg = WebhookMessage::CreateFolder {
|
||||
workspace: "demo".to_string(),
|
||||
name: "shared".to_string(),
|
||||
};
|
||||
let json = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(json["type"], "CreateFolder");
|
||||
assert_eq!(json["name"], "shared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webhook_message_resource_type() {
|
||||
let msg = WebhookMessage::CreateResourceType {
|
||||
name: "postgresql".to_string(),
|
||||
};
|
||||
let json = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(json["type"], "CreateResourceType");
|
||||
assert_eq!(json["name"], "postgresql");
|
||||
// Should NOT have workspace field
|
||||
assert!(json.get("workspace").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webhook_message_all_variants_have_type_tag() {
|
||||
let messages: Vec<WebhookMessage> = vec![
|
||||
WebhookMessage::CreateApp { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::DeleteApp { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::UpdateApp { workspace: "w".into(), old_path: "o".into(), new_path: "n".into() },
|
||||
WebhookMessage::CreateFlow { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::UpdateFlow { workspace: "w".into(), old_path: "o".into(), new_path: "n".into() },
|
||||
WebhookMessage::ArchiveFlow { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::DeleteFlow { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::CreateFolder { workspace: "w".into(), name: "n".into() },
|
||||
WebhookMessage::UpdateFolder { workspace: "w".into(), name: "n".into() },
|
||||
WebhookMessage::DeleteFolder { workspace: "w".into(), name: "n".into() },
|
||||
WebhookMessage::DeleteResource { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::CreateResource { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::UpdateResource { workspace: "w".into(), old_path: "o".into(), new_path: "n".into() },
|
||||
WebhookMessage::CreateResourceType { name: "n".into() },
|
||||
WebhookMessage::DeleteResourceType { name: "n".into() },
|
||||
WebhookMessage::UpdateResourceType { name: "n".into() },
|
||||
WebhookMessage::CreateScript { workspace: "w".into(), path: "p".into(), hash: "h".into() },
|
||||
WebhookMessage::UpdateScript { workspace: "w".into(), path: "p".into(), hash: "h".into() },
|
||||
WebhookMessage::DeleteScript { workspace: "w".into(), hash: "h".into() },
|
||||
WebhookMessage::DeleteScriptPath { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::CreateVariable { workspace: "w".into(), path: "p".into() },
|
||||
WebhookMessage::UpdateVariable { workspace: "w".into(), old_path: "o".into(), new_path: "n".into() },
|
||||
WebhookMessage::DeleteVariable { workspace: "w".into(), path: "p".into() },
|
||||
];
|
||||
|
||||
for msg in &messages {
|
||||
let json = serde_json::to_value(msg).unwrap();
|
||||
assert!(
|
||||
json.get("type").is_some(),
|
||||
"Missing 'type' tag in: {}",
|
||||
serde_json::to_string(msg).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webhook_message_type_tags_are_variant_names() {
|
||||
let msg = WebhookMessage::CreateApp {
|
||||
workspace: "w".into(),
|
||||
path: "p".into(),
|
||||
};
|
||||
let json = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(json["type"], "CreateApp");
|
||||
|
||||
let msg = WebhookMessage::DeleteVariable {
|
||||
workspace: "w".into(),
|
||||
path: "p".into(),
|
||||
};
|
||||
let json = serde_json::to_value(&msg).unwrap();
|
||||
assert_eq!(json["type"], "DeleteVariable");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -410,4 +410,186 @@ mod tests {
|
||||
let config: HttpConfigRequest = serde_json::from_str(json_both).unwrap();
|
||||
assert_eq!(config.request_type, RequestType::SyncSse);
|
||||
}
|
||||
|
||||
// --- HttpMethod ---
|
||||
|
||||
#[test]
|
||||
fn test_http_method_from_http_get() {
|
||||
let method = HttpMethod::try_from(&http::Method::GET).unwrap();
|
||||
assert_eq!(method, HttpMethod::Get);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_method_from_http_post() {
|
||||
let method = HttpMethod::try_from(&http::Method::POST).unwrap();
|
||||
assert_eq!(method, HttpMethod::Post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_method_from_http_put() {
|
||||
let method = HttpMethod::try_from(&http::Method::PUT).unwrap();
|
||||
assert_eq!(method, HttpMethod::Put);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_method_from_http_delete() {
|
||||
let method = HttpMethod::try_from(&http::Method::DELETE).unwrap();
|
||||
assert_eq!(method, HttpMethod::Delete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_method_from_http_patch() {
|
||||
let method = HttpMethod::try_from(&http::Method::PATCH).unwrap();
|
||||
assert_eq!(method, HttpMethod::Patch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_method_unsupported() {
|
||||
let result = HttpMethod::try_from(&http::Method::HEAD);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_method_options_unsupported() {
|
||||
let result = HttpMethod::try_from(&http::Method::OPTIONS);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// --- HttpMethod serde ---
|
||||
|
||||
#[test]
|
||||
fn test_http_method_serde_roundtrip() {
|
||||
for method in [HttpMethod::Get, HttpMethod::Post, HttpMethod::Put, HttpMethod::Delete, HttpMethod::Patch] {
|
||||
let json = serde_json::to_value(method).unwrap();
|
||||
let deserialized: HttpMethod = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(method, deserialized);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_method_serialize_lowercase() {
|
||||
assert_eq!(serde_json::to_value(HttpMethod::Get).unwrap(), "get");
|
||||
assert_eq!(serde_json::to_value(HttpMethod::Post).unwrap(), "post");
|
||||
}
|
||||
|
||||
// --- RequestType serde ---
|
||||
|
||||
#[test]
|
||||
fn test_request_type_serde_roundtrip() {
|
||||
for rt in [RequestType::Sync, RequestType::Async, RequestType::SyncSse] {
|
||||
let json = serde_json::to_value(rt).unwrap();
|
||||
let deserialized: RequestType = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(rt, deserialized);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request_type_serialize_values() {
|
||||
assert_eq!(serde_json::to_value(RequestType::Sync).unwrap(), "sync");
|
||||
assert_eq!(serde_json::to_value(RequestType::Async).unwrap(), "async");
|
||||
assert_eq!(serde_json::to_value(RequestType::SyncSse).unwrap(), "sync_sse");
|
||||
}
|
||||
|
||||
// --- AuthenticationMethod serde ---
|
||||
|
||||
#[test]
|
||||
fn test_authentication_method_serde_roundtrip() {
|
||||
for method in [
|
||||
AuthenticationMethod::None,
|
||||
AuthenticationMethod::Windmill,
|
||||
AuthenticationMethod::ApiKey,
|
||||
AuthenticationMethod::BasicHttp,
|
||||
AuthenticationMethod::CustomScript,
|
||||
AuthenticationMethod::Signature,
|
||||
] {
|
||||
let json = serde_json::to_value(method).unwrap();
|
||||
let deserialized: AuthenticationMethod = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(method, deserialized);
|
||||
}
|
||||
}
|
||||
|
||||
// --- validate_authentication_method ---
|
||||
|
||||
#[test]
|
||||
fn test_validate_auth_none_ok() {
|
||||
assert!(validate_authentication_method(AuthenticationMethod::None, None).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_auth_windmill_ok() {
|
||||
assert!(validate_authentication_method(AuthenticationMethod::Windmill, None).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_auth_custom_script_requires_raw() {
|
||||
assert!(validate_authentication_method(AuthenticationMethod::CustomScript, None).is_err());
|
||||
assert!(validate_authentication_method(AuthenticationMethod::CustomScript, Some(false)).is_err());
|
||||
assert!(validate_authentication_method(AuthenticationMethod::CustomScript, Some(true)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_auth_signature_without_raw_ok() {
|
||||
assert!(validate_authentication_method(AuthenticationMethod::Signature, None).is_ok());
|
||||
}
|
||||
|
||||
// --- Route path regex ---
|
||||
|
||||
#[test]
|
||||
fn test_valid_route_path() {
|
||||
assert!(VALID_ROUTE_PATH_RE.is_match("users"));
|
||||
assert!(VALID_ROUTE_PATH_RE.is_match("users/:id"));
|
||||
assert!(VALID_ROUTE_PATH_RE.is_match("api/v1/users"));
|
||||
assert!(VALID_ROUTE_PATH_RE.is_match("api/v1/:id"));
|
||||
assert!(VALID_ROUTE_PATH_RE.is_match("files/*path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_route_path() {
|
||||
assert!(!VALID_ROUTE_PATH_RE.is_match(""));
|
||||
assert!(!VALID_ROUTE_PATH_RE.is_match("/leading-slash"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_path_key_regex() {
|
||||
assert!(ROUTE_PATH_KEY_RE.is_match("/:id"));
|
||||
assert!(ROUTE_PATH_KEY_RE.is_match("/*path"));
|
||||
assert!(ROUTE_PATH_KEY_RE.is_match("/users/:userId/posts/:postId"));
|
||||
}
|
||||
|
||||
// --- HttpConfig deserialization ---
|
||||
|
||||
#[test]
|
||||
fn test_http_config_request_full() {
|
||||
let json = r#"{
|
||||
"route_path": "api/v1/users",
|
||||
"request_type": "async",
|
||||
"authentication_method": "api_key",
|
||||
"http_method": "post",
|
||||
"is_static_website": false,
|
||||
"workspaced_route": true,
|
||||
"wrap_body": true,
|
||||
"raw_string": false
|
||||
}"#;
|
||||
let config: HttpConfigRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(config.route_path, "api/v1/users");
|
||||
assert_eq!(config.request_type, RequestType::Async);
|
||||
assert_eq!(config.authentication_method, AuthenticationMethod::ApiKey);
|
||||
assert_eq!(config.http_method, HttpMethod::Post);
|
||||
assert_eq!(config.workspaced_route, Some(true));
|
||||
assert_eq!(config.wrap_body, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_config_request_minimal() {
|
||||
let json = r#"{
|
||||
"authentication_method": "none",
|
||||
"http_method": "get",
|
||||
"is_static_website": false
|
||||
}"#;
|
||||
let config: HttpConfigRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(config.route_path, "");
|
||||
assert_eq!(config.request_type, RequestType::Sync);
|
||||
assert!(config.workspaced_route.is_none());
|
||||
assert!(config.summary.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,3 +22,36 @@ pub fn parse_bool(s: &str) -> Result<bool, ParseBoolError> {
|
||||
_ => Err(ParseBoolError::InvalidInput(s.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_true() {
|
||||
assert_eq!(parse_bool("t").unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_false() {
|
||||
assert_eq!(parse_bool("f").unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_true_string() {
|
||||
assert!(matches!(
|
||||
parse_bool("true"),
|
||||
Err(ParseBoolError::InvalidInput(s)) if s == "true"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_empty() {
|
||||
assert!(parse_bool("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_uppercase() {
|
||||
assert!(parse_bool("T").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,3 +253,244 @@ impl Converter {
|
||||
Ok(arr)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
// --- Scalar type conversions ---
|
||||
|
||||
#[test]
|
||||
fn test_bool_true() {
|
||||
let result = Converter::try_from_str(Some(Type::BOOL), "t").unwrap();
|
||||
assert_eq!(result, Value::Bool(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bool_false() {
|
||||
let result = Converter::try_from_str(Some(Type::BOOL), "f").unwrap();
|
||||
assert_eq!(result, Value::Bool(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text() {
|
||||
let result = Converter::try_from_str(Some(Type::TEXT), "hello world").unwrap();
|
||||
assert_eq!(result, Value::String("hello world".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_varchar() {
|
||||
let result = Converter::try_from_str(Some(Type::VARCHAR), "test").unwrap();
|
||||
assert_eq!(result, Value::String("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int2() {
|
||||
let result = Converter::try_from_str(Some(Type::INT2), "42").unwrap();
|
||||
assert_eq!(result, json!(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int4() {
|
||||
let result = Converter::try_from_str(Some(Type::INT4), "-100").unwrap();
|
||||
assert_eq!(result, json!(-100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int8() {
|
||||
let result = Converter::try_from_str(Some(Type::INT8), "9999999999").unwrap();
|
||||
assert_eq!(result, json!(9999999999i64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float4() {
|
||||
let result = Converter::try_from_str(Some(Type::FLOAT4), "3.14").unwrap();
|
||||
assert!(result.as_f64().unwrap() - 3.14 < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float8() {
|
||||
let result = Converter::try_from_str(Some(Type::FLOAT8), "2.718281828").unwrap();
|
||||
assert!(result.as_f64().unwrap() - 2.718281828 < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_numeric() {
|
||||
let result = Converter::try_from_str(Some(Type::NUMERIC), "123.456").unwrap();
|
||||
assert_eq!(result.to_string(), "123.456");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uuid() {
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::UUID), "550e8400-e29b-41d4-a716-446655440000")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Value::String("550e8400-e29b-41d4-a716-446655440000".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json() {
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::JSON), r#"{"key": "value", "n": 1}"#).unwrap();
|
||||
assert_eq!(result, json!({"key": "value", "n": 1}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jsonb() {
|
||||
let result = Converter::try_from_str(Some(Type::JSONB), r#"[1,2,3]"#).unwrap();
|
||||
assert_eq!(result, json!([1, 2, 3]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date() {
|
||||
let result = Converter::try_from_str(Some(Type::DATE), "2024-01-15").unwrap();
|
||||
assert_eq!(result, Value::String("2024-01-15".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time() {
|
||||
let result = Converter::try_from_str(Some(Type::TIME), "14:30:00.0").unwrap();
|
||||
assert_eq!(result, Value::String("14:30:00".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp() {
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::TIMESTAMP), "2024-01-15 14:30:00.0").unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Value::String("2024-01-15 14:30:00".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamptz() {
|
||||
let result = Converter::try_from_str(
|
||||
Some(Type::TIMESTAMPTZ),
|
||||
"2024-01-15 14:30:00.0+00",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(result.as_str().unwrap().contains("2024-01-15"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bytea() {
|
||||
let result = Converter::try_from_str(Some(Type::BYTEA), "\\x48656c6c6f").unwrap();
|
||||
assert_eq!(result, json!([72, 101, 108, 108, 111]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oid() {
|
||||
let result = Converter::try_from_str(Some(Type::OID), "12345").unwrap();
|
||||
assert_eq!(result, json!(12345u32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_none_type_defaults_to_text() {
|
||||
let result = Converter::try_from_str(None, "anything").unwrap();
|
||||
assert_eq!(result, Value::String("anything".to_string()));
|
||||
}
|
||||
|
||||
// --- Array type conversions ---
|
||||
|
||||
#[test]
|
||||
fn test_int4_array() {
|
||||
let result = Converter::try_from_str(Some(Type::INT4_ARRAY), "{1,2,3}").unwrap();
|
||||
assert_eq!(result, json!([1, 2, 3]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_array() {
|
||||
let result = Converter::try_from_str(Some(Type::TEXT_ARRAY), "{hello,world}").unwrap();
|
||||
assert_eq!(result, json!(["hello", "world"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bool_array() {
|
||||
let result = Converter::try_from_str(Some(Type::BOOL_ARRAY), "{t,f,t}").unwrap();
|
||||
assert_eq!(result, json!([true, false, true]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_with_null() {
|
||||
let result = Converter::try_from_str(Some(Type::INT4_ARRAY), "{1,NULL,3}").unwrap();
|
||||
assert_eq!(result, json!([1, null, 3]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_with_quoted_strings() {
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"hello, world","test"}"#).unwrap();
|
||||
assert_eq!(result, json!(["hello, world", "test"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_array() {
|
||||
let result = Converter::try_from_str(Some(Type::INT4_ARRAY), "{}").unwrap();
|
||||
assert_eq!(result, json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uuid_array() {
|
||||
let result = Converter::try_from_str(
|
||||
Some(Type::UUID_ARRAY),
|
||||
"{550e8400-e29b-41d4-a716-446655440000,6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
json!([
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// --- Error cases ---
|
||||
|
||||
#[test]
|
||||
fn test_invalid_int() {
|
||||
assert!(Converter::try_from_str(Some(Type::INT4), "not_a_number").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_bool() {
|
||||
assert!(Converter::try_from_str(Some(Type::BOOL), "yes").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_uuid() {
|
||||
assert!(Converter::try_from_str(Some(Type::UUID), "not-a-uuid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_json() {
|
||||
assert!(Converter::try_from_str(Some(Type::JSON), "not json").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_missing_braces() {
|
||||
assert!(Converter::try_from_str(Some(Type::INT4_ARRAY), "1,2,3").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_too_short() {
|
||||
assert!(Converter::try_from_str(Some(Type::INT4_ARRAY), "{").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_with_escaped_backslash() {
|
||||
let result =
|
||||
Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"a\\b","c"}"#).unwrap();
|
||||
assert_eq!(result, json!(["a\\b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_nan_rejected() {
|
||||
assert!(Converter::try_from_str(Some(Type::FLOAT4), "NaN").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,86 @@ pub fn from_bytea_hex(s: &str) -> Result<Vec<u8>, ByteaHexParseError> {
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_bytea_hex() {
|
||||
assert_eq!(from_bytea_hex("\\x48656c6c6f").unwrap(), b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_bytea_hex() {
|
||||
assert_eq!(from_bytea_hex("\\x").unwrap(), Vec::<u8>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_byte() {
|
||||
assert_eq!(from_bytea_hex("\\xff").unwrap(), vec![0xff]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_zeros() {
|
||||
assert_eq!(from_bytea_hex("\\x000000").unwrap(), vec![0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_prefix() {
|
||||
assert!(matches!(
|
||||
from_bytea_hex("48656c6c6f"),
|
||||
Err(ByteaHexParseError::InvalidPrefix)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrong_prefix() {
|
||||
assert!(matches!(
|
||||
from_bytea_hex("0x48656c6c6f"),
|
||||
Err(ByteaHexParseError::InvalidPrefix)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_too_short() {
|
||||
assert!(matches!(
|
||||
from_bytea_hex("\\"),
|
||||
Err(ByteaHexParseError::InvalidPrefix)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_string() {
|
||||
assert!(matches!(
|
||||
from_bytea_hex(""),
|
||||
Err(ByteaHexParseError::InvalidPrefix)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_odd_digits() {
|
||||
assert!(matches!(
|
||||
from_bytea_hex("\\xabc"),
|
||||
Err(ByteaHexParseError::OddNumerOfDigits)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_hex_chars() {
|
||||
assert!(matches!(
|
||||
from_bytea_hex("\\xzz"),
|
||||
Err(ByteaHexParseError::ParseInt(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uppercase_hex() {
|
||||
assert_eq!(from_bytea_hex("\\xABCD").unwrap(), vec![0xab, 0xcd]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_case_hex() {
|
||||
assert_eq!(from_bytea_hex("\\xAbCd").unwrap(), vec![0xab, 0xcd]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,3 +550,163 @@ pub fn generate_random_string() -> String {
|
||||
|
||||
format!("{}_{}", timestamp, random_part)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_publication_data() {
|
||||
let json = r#"{
|
||||
"table_to_track": [
|
||||
{
|
||||
"schema_name": "public",
|
||||
"table_to_track": [
|
||||
{"table_name": "users"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"transaction_to_track": ["insert", "update"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_publication_data_empty_schema_name() {
|
||||
let json = r#"{
|
||||
"table_to_track": [
|
||||
{
|
||||
"schema_name": "",
|
||||
"table_to_track": []
|
||||
}
|
||||
],
|
||||
"transaction_to_track": ["insert"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_publication_data_empty_table_name() {
|
||||
let json = r#"{
|
||||
"table_to_track": [
|
||||
{
|
||||
"schema_name": "public",
|
||||
"table_to_track": [
|
||||
{"table_name": " "}
|
||||
]
|
||||
}
|
||||
],
|
||||
"transaction_to_track": ["insert"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_publication_data_invalid_transaction_type() {
|
||||
let json = r#"{
|
||||
"transaction_to_track": ["insert", "truncate"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_publication_data_too_many_transaction_types() {
|
||||
let json = r#"{
|
||||
"transaction_to_track": ["insert", "update", "delete", "insert"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_publication_data_duplicate_schema_names() {
|
||||
let json = r#"{
|
||||
"table_to_track": [
|
||||
{"schema_name": "public", "table_to_track": [{"table_name": "a"}]},
|
||||
{"schema_name": "public", "table_to_track": [{"table_name": "b"}]}
|
||||
],
|
||||
"transaction_to_track": ["insert"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_publication_data_all_tables_in_schema() {
|
||||
let json = r#"{
|
||||
"table_to_track": [
|
||||
{"schema_name": "public", "table_to_track": []}
|
||||
],
|
||||
"transaction_to_track": ["insert"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_publication_data_incompatible_tracking() {
|
||||
let json = r#"{
|
||||
"table_to_track": [
|
||||
{"schema_name": "schema1", "table_to_track": []},
|
||||
{"schema_name": "schema2", "table_to_track": [{"table_name": "t1", "columns_name": ["col1"]}]}
|
||||
],
|
||||
"transaction_to_track": ["insert"]
|
||||
}"#;
|
||||
let result: std::result::Result<PublicationData, _> = serde_json::from_str(json);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_config_serialization() {
|
||||
let config = PostgresConfig {
|
||||
postgres_resource_path: "f/db/postgres".to_string(),
|
||||
replication_slot_name: "slot_1".to_string(),
|
||||
publication_name: "pub_1".to_string(),
|
||||
basic_mode: Some(false),
|
||||
};
|
||||
let json = serde_json::to_value(&config).unwrap();
|
||||
assert_eq!(json["postgres_resource_path"], "f/db/postgres");
|
||||
assert_eq!(json["replication_slot_name"], "slot_1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_random_string_format() {
|
||||
let s = generate_random_string();
|
||||
assert!(s.contains('_'));
|
||||
let parts: Vec<&str> = s.split('_').collect();
|
||||
assert_eq!(parts.len(), 2);
|
||||
assert_eq!(parts[1].len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relations_add_table() {
|
||||
let mut rel = Relations::new("public".to_string(), vec![]);
|
||||
rel.add_new_table(TableToTrack::new("users".to_string(), None, None));
|
||||
assert_eq!(rel.table_to_track.len(), 1);
|
||||
assert_eq!(rel.table_to_track[0].table_name, "users");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_to_track_with_where_clause() {
|
||||
let tt = TableToTrack::new(
|
||||
"orders".to_string(),
|
||||
Some("status = 'active'".to_string()),
|
||||
None,
|
||||
);
|
||||
assert_eq!(tt.where_clause, Some("status = 'active'".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_to_track_with_columns() {
|
||||
let tt = TableToTrack::new(
|
||||
"users".to_string(),
|
||||
None,
|
||||
Some(vec!["id".to_string(), "email".to_string()]),
|
||||
);
|
||||
assert_eq!(tt.columns_name.as_ref().unwrap().len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,3 +133,90 @@ export async function main(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_postgres_to_typescript_bool() {
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::BOOL)), "boolean");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_to_typescript_text_types() {
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::TEXT)), "string");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::VARCHAR)), "string");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::CHAR)), "string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_to_typescript_number_types() {
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::INT2)), "number");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::INT4)), "number");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::INT8)), "number");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::FLOAT4)), "number");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::FLOAT8)), "number");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::NUMERIC)), "number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_to_typescript_array_types() {
|
||||
assert_eq!(
|
||||
postgres_to_typescript_type(Some(Type::INT4_ARRAY)),
|
||||
"Array<number>"
|
||||
);
|
||||
assert_eq!(
|
||||
postgres_to_typescript_type(Some(Type::TEXT_ARRAY)),
|
||||
"Array<string>"
|
||||
);
|
||||
assert_eq!(
|
||||
postgres_to_typescript_type(Some(Type::BOOL_ARRAY)),
|
||||
"Array<boolean>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_to_typescript_date_types() {
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::DATE)), "string");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMP)), "string");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMPTZ)), "string");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::UUID)), "string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_to_typescript_json() {
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::JSON)), "unknown");
|
||||
assert_eq!(postgres_to_typescript_type(Some(Type::JSONB)), "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postgres_to_typescript_none() {
|
||||
assert_eq!(postgres_to_typescript_type(None), "string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_body_struct_typescript() {
|
||||
let fields = vec![
|
||||
MappingInfo::new("id".to_string(), Some(Type::INT4), false),
|
||||
MappingInfo::new("name".to_string(), Some(Type::TEXT), true),
|
||||
];
|
||||
let result = into_body_struct(Language::Typescript, fields);
|
||||
assert!(result.contains("id: number,"));
|
||||
assert!(result.contains("name?: string,"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_template() {
|
||||
let mapper = Mapper::new(HashMap::new(), Language::Typescript);
|
||||
let template = mapper.get_template();
|
||||
assert!(template.contains("row: any"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mapping_info_nullable_field() {
|
||||
let info = MappingInfo::new("email".to_string(), Some(Type::VARCHAR), true);
|
||||
assert!(info.is_nullable);
|
||||
assert_eq!(info.column_name, "email");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,3 +72,117 @@ impl RelationConverter {
|
||||
Ok(object)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
use rust_postgres::types::Type;
|
||||
use serde_json::json;
|
||||
|
||||
use super::super::replication_message::{Column, ReplicaIdentity, RelationBody};
|
||||
|
||||
fn make_relation(o_id: Oid, columns: Vec<Column>) -> RelationBody {
|
||||
RelationBody::new(None, o_id, "public".to_string(), "test".to_string(), ReplicaIdentity::Default, columns)
|
||||
}
|
||||
|
||||
fn text_col(name: &str, typ: Option<Type>) -> Column {
|
||||
Column::new(0, name.to_string(), typ, -1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_to_json_text_columns() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("id", Some(Type::INT4)),
|
||||
text_col("name", Some(Type::TEXT)),
|
||||
]));
|
||||
|
||||
let tuple = vec![
|
||||
TupleData::Text(Bytes::from("42")),
|
||||
TupleData::Text(Bytes::from("Alice")),
|
||||
];
|
||||
|
||||
let result = converter.row_to_json((1, tuple)).unwrap();
|
||||
assert_eq!(result["id"], json!(42));
|
||||
assert_eq!(result["name"], json!("Alice"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_to_json_with_null() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("id", Some(Type::INT4)),
|
||||
text_col("email", Some(Type::TEXT)),
|
||||
]));
|
||||
|
||||
let tuple = vec![
|
||||
TupleData::Text(Bytes::from("1")),
|
||||
TupleData::Null,
|
||||
];
|
||||
|
||||
let result = converter.row_to_json((1, tuple)).unwrap();
|
||||
assert_eq!(result["id"], json!(1));
|
||||
assert_eq!(result["email"], Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_to_json_with_unchanged_toast() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("data", Some(Type::TEXT)),
|
||||
]));
|
||||
|
||||
let tuple = vec![TupleData::UnchangedToast];
|
||||
let result = converter.row_to_json((1, tuple)).unwrap();
|
||||
assert_eq!(result["data"], Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_to_json_binary_rejected() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("data", Some(Type::BYTEA)),
|
||||
]));
|
||||
|
||||
let tuple = vec![TupleData::Binary(Bytes::from("data"))];
|
||||
assert!(matches!(
|
||||
converter.row_to_json((1, tuple)),
|
||||
Err(RelationConversionError::BinaryFormatNotSupported)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_relation() {
|
||||
let converter = RelationConverter::new();
|
||||
let tuple = vec![TupleData::Null];
|
||||
assert!(matches!(
|
||||
converter.row_to_json((999, tuple)),
|
||||
Err(RelationConversionError::FailToFindMatchingTable)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_relations() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![text_col("a", Some(Type::TEXT))]));
|
||||
converter.add_relation(make_relation(2, vec![text_col("b", Some(Type::INT4))]));
|
||||
|
||||
let result1 = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("hello"))])).unwrap();
|
||||
let result2 = converter.row_to_json((2, vec![TupleData::Text(Bytes::from("42"))])).unwrap();
|
||||
|
||||
assert_eq!(result1["a"], json!("hello"));
|
||||
assert_eq!(result2["b"], json!(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_to_json_bool_column() {
|
||||
let mut converter = RelationConverter::new();
|
||||
converter.add_relation(make_relation(1, vec![
|
||||
text_col("active", Some(Type::BOOL)),
|
||||
]));
|
||||
|
||||
let result = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("t"))])).unwrap();
|
||||
assert_eq!(result["active"], json!(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,3 +508,309 @@ impl ReplicationMessage {
|
||||
Ok(replication_message)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn build_keepalive(wal_end: u64, timestamp: i64, reply: bool) -> Bytes {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(PRIMARY_KEEPALIVE_BYTE);
|
||||
buf.extend_from_slice(&wal_end.to_be_bytes());
|
||||
buf.extend_from_slice(×tamp.to_be_bytes());
|
||||
buf.push(if reply { 1 } else { 0 });
|
||||
Bytes::from(buf)
|
||||
}
|
||||
|
||||
fn build_xlog_data(wal_start: u64, wal_end: u64, timestamp: i64, data: &[u8]) -> Bytes {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(X_LOG_DATA_BYTE);
|
||||
buf.extend_from_slice(&wal_start.to_be_bytes());
|
||||
buf.extend_from_slice(&wal_end.to_be_bytes());
|
||||
buf.extend_from_slice(×tamp.to_be_bytes());
|
||||
buf.extend_from_slice(data);
|
||||
Bytes::from(buf)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_keepalive_with_reply() {
|
||||
let buf = build_keepalive(100, 200, true);
|
||||
match ReplicationMessage::parse(buf).unwrap() {
|
||||
ReplicationMessage::PrimaryKeepAlive(body) => {
|
||||
assert_eq!(body.wal_end, 100);
|
||||
assert_eq!(body.timestamp, 200);
|
||||
assert!(body.reply);
|
||||
}
|
||||
_ => panic!("expected PrimaryKeepAlive"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_keepalive_without_reply() {
|
||||
let buf = build_keepalive(500, 1000, false);
|
||||
match ReplicationMessage::parse(buf).unwrap() {
|
||||
ReplicationMessage::PrimaryKeepAlive(body) => {
|
||||
assert_eq!(body.wal_end, 500);
|
||||
assert_eq!(body.timestamp, 1000);
|
||||
assert!(!body.reply);
|
||||
}
|
||||
_ => panic!("expected PrimaryKeepAlive"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_xlog_data() {
|
||||
let payload = b"test payload";
|
||||
let buf = build_xlog_data(10, 20, 30, payload);
|
||||
match ReplicationMessage::parse(buf).unwrap() {
|
||||
ReplicationMessage::XLogData(body) => {
|
||||
assert_eq!(body.wal_start, 10);
|
||||
assert_eq!(body.wal_end, 20);
|
||||
assert_eq!(body.timestamp, 30);
|
||||
assert_eq!(&body.data[..], payload);
|
||||
}
|
||||
_ => panic!("expected XLogData"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unknown_byte() {
|
||||
let buf = Bytes::from(vec![0xFF, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
assert!(ReplicationMessage::parse(buf).is_err());
|
||||
}
|
||||
|
||||
fn build_begin_message() -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(BEGIN_BYTE);
|
||||
buf.extend_from_slice(&0i64.to_be_bytes()); // lsn
|
||||
buf.extend_from_slice(&0i64.to_be_bytes()); // timestamp
|
||||
buf.extend_from_slice(&0i32.to_be_bytes()); // xid
|
||||
buf
|
||||
}
|
||||
|
||||
fn build_commit_message() -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(COMMIT_BYTE);
|
||||
buf.push(0); // flags
|
||||
buf.extend_from_slice(&0u64.to_be_bytes()); // lsn
|
||||
buf.extend_from_slice(&0u64.to_be_bytes()); // end_lsn
|
||||
buf.extend_from_slice(&0i64.to_be_bytes()); // timestamp
|
||||
buf
|
||||
}
|
||||
|
||||
fn build_insert_message(o_id: u32, tuple_data: &[(u8, &[u8])]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(INSERT_BYTE);
|
||||
buf.extend_from_slice(&o_id.to_be_bytes());
|
||||
buf.push(TUPLE_NEW_BYTE);
|
||||
buf.extend_from_slice(&(tuple_data.len() as i16).to_be_bytes());
|
||||
for (tag, data) in tuple_data {
|
||||
buf.push(*tag);
|
||||
match *tag {
|
||||
TUPLE_DATA_TEXT_BYTE | TUPLE_DATA_BINARY_BYTE => {
|
||||
buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
|
||||
buf.extend_from_slice(data);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
fn build_delete_message(o_id: u32, tuple_data: &[(u8, &[u8])]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(DELETE_BYTE);
|
||||
buf.extend_from_slice(&o_id.to_be_bytes());
|
||||
buf.push(TUPLE_OLD_BYTE);
|
||||
buf.extend_from_slice(&(tuple_data.len() as i16).to_be_bytes());
|
||||
for (tag, data) in tuple_data {
|
||||
buf.push(*tag);
|
||||
match *tag {
|
||||
TUPLE_DATA_TEXT_BYTE | TUPLE_DATA_BINARY_BYTE => {
|
||||
buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
|
||||
buf.extend_from_slice(data);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
fn settings(streaming: bool) -> LogicalReplicationSettings {
|
||||
LogicalReplicationSettings { streaming }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_begin() {
|
||||
let data = Bytes::from(build_begin_message());
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Begin => {}
|
||||
other => panic!("expected Begin, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_commit() {
|
||||
let data = Bytes::from(build_commit_message());
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Commit => {}
|
||||
other => panic!("expected Commit, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_insert_with_text_tuple() {
|
||||
let data = Bytes::from(build_insert_message(
|
||||
42,
|
||||
&[(TUPLE_DATA_TEXT_BYTE, b"hello")],
|
||||
));
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Insert(insert) => {
|
||||
assert_eq!(insert.o_id, 42);
|
||||
assert_eq!(insert.tuple.len(), 1);
|
||||
match &insert.tuple[0] {
|
||||
TupleData::Text(b) => assert_eq!(&b[..], b"hello"),
|
||||
other => panic!("expected Text, got {:?}", other),
|
||||
}
|
||||
}
|
||||
other => panic!("expected Insert, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_insert_with_null_tuple() {
|
||||
let data = Bytes::from(build_insert_message(
|
||||
10,
|
||||
&[(TUPLE_DATA_NULL_BYTE, &[])],
|
||||
));
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Insert(insert) => {
|
||||
assert_eq!(insert.tuple.len(), 1);
|
||||
assert!(matches!(insert.tuple[0], TupleData::Null));
|
||||
}
|
||||
other => panic!("expected Insert, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_insert_with_toast_tuple() {
|
||||
let data = Bytes::from(build_insert_message(
|
||||
10,
|
||||
&[(TUPLE_DATA_TOAST_BYTE, &[])],
|
||||
));
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Insert(insert) => {
|
||||
assert!(matches!(insert.tuple[0], TupleData::UnchangedToast));
|
||||
}
|
||||
other => panic!("expected Insert, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_insert_multiple_columns() {
|
||||
let data = Bytes::from(build_insert_message(
|
||||
1,
|
||||
&[
|
||||
(TUPLE_DATA_TEXT_BYTE, b"col1"),
|
||||
(TUPLE_DATA_NULL_BYTE, &[]),
|
||||
(TUPLE_DATA_TEXT_BYTE, b"col3"),
|
||||
],
|
||||
));
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Insert(insert) => {
|
||||
assert_eq!(insert.tuple.len(), 3);
|
||||
assert!(matches!(&insert.tuple[0], TupleData::Text(_)));
|
||||
assert!(matches!(insert.tuple[1], TupleData::Null));
|
||||
assert!(matches!(&insert.tuple[2], TupleData::Text(_)));
|
||||
}
|
||||
other => panic!("expected Insert, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_delete_with_old_tuple() {
|
||||
let data = Bytes::from(build_delete_message(
|
||||
99,
|
||||
&[(TUPLE_DATA_TEXT_BYTE, b"old_val")],
|
||||
));
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Delete(delete) => {
|
||||
assert_eq!(delete.o_id, 99);
|
||||
assert!(delete.old_tuple.is_some());
|
||||
assert!(delete.key_tuple.is_none());
|
||||
}
|
||||
other => panic!("expected Delete, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_relation() {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(RELATION_BYTE);
|
||||
buf.extend_from_slice(&100u32.to_be_bytes()); // o_id
|
||||
buf.extend_from_slice(b"public\0"); // namespace
|
||||
buf.extend_from_slice(b"users\0"); // name
|
||||
buf.push(REPLICA_IDENTITY_DEFAULT_BYTE as u8); // replica identity
|
||||
buf.extend_from_slice(&1i16.to_be_bytes()); // num columns
|
||||
// column: flags=0, name="id", type_oid=23 (INT4), type_modifier=-1
|
||||
buf.push(0); // flags
|
||||
buf.extend_from_slice(b"id\0"); // name
|
||||
buf.extend_from_slice(&23u32.to_be_bytes()); // type_oid (INT4)
|
||||
buf.extend_from_slice(&(-1i32).to_be_bytes()); // type_modifier
|
||||
|
||||
let data = Bytes::from(buf);
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(false)).unwrap() {
|
||||
LogicalReplicationMessage::Relation(rel) => {
|
||||
assert_eq!(rel.o_id, 100);
|
||||
assert_eq!(rel.namespace, "public");
|
||||
assert_eq!(rel.name, "users");
|
||||
assert_eq!(rel.columns.len(), 1);
|
||||
assert_eq!(rel.columns[0].name, "id");
|
||||
assert!(matches!(rel.columns[0].type_o_id, Some(Type::INT4)));
|
||||
}
|
||||
other => panic!("expected Relation, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_insert_with_streaming_transaction_id() {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(INSERT_BYTE);
|
||||
buf.extend_from_slice(&42i32.to_be_bytes()); // transaction_id
|
||||
buf.extend_from_slice(&10u32.to_be_bytes()); // o_id
|
||||
buf.push(TUPLE_NEW_BYTE);
|
||||
buf.extend_from_slice(&0i16.to_be_bytes()); // 0 columns
|
||||
|
||||
let data = Bytes::from(buf);
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
match body.parse(&settings(true)).unwrap() {
|
||||
LogicalReplicationMessage::Insert(insert) => {
|
||||
assert_eq!(insert.transaction_id, Some(42));
|
||||
assert_eq!(insert.o_id, 10);
|
||||
}
|
||||
other => panic!("expected Insert, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_tuple_data_byte() {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(INSERT_BYTE);
|
||||
buf.extend_from_slice(&1u32.to_be_bytes()); // o_id
|
||||
buf.push(TUPLE_NEW_BYTE);
|
||||
buf.extend_from_slice(&1i16.to_be_bytes()); // 1 column
|
||||
buf.push(0xFF); // invalid tuple data byte
|
||||
|
||||
let data = Bytes::from(buf);
|
||||
let body = XLogDataBody::new(0, 0, 0, data);
|
||||
assert!(body.parse(&settings(false)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +139,82 @@ mod tests {
|
||||
let result = is_value_superset(&mut deserializer, key, &value).unwrap();
|
||||
assert!(!result, "Should not match when key doesn't exist");
|
||||
}
|
||||
|
||||
// --- is_superset unit tests ---
|
||||
|
||||
#[test]
|
||||
fn test_superset_equal_scalars() {
|
||||
assert!(is_superset(&json!(42), &json!(42)));
|
||||
assert!(is_superset(&json!("hello"), &json!("hello")));
|
||||
assert!(is_superset(&json!(true), &json!(true)));
|
||||
assert!(is_superset(&json!(null), &json!(null)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_unequal_scalars() {
|
||||
assert!(!is_superset(&json!(42), &json!(43)));
|
||||
assert!(!is_superset(&json!("hello"), &json!("world")));
|
||||
assert!(!is_superset(&json!(true), &json!(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_object_subset() {
|
||||
let full = json!({"a": 1, "b": 2, "c": 3});
|
||||
let subset = json!({"a": 1, "b": 2});
|
||||
assert!(is_superset(&full, &subset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_object_not_subset() {
|
||||
let full = json!({"a": 1, "b": 2});
|
||||
let check = json!({"a": 1, "b": 3});
|
||||
assert!(!is_superset(&full, &check));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_object_missing_key() {
|
||||
let full = json!({"a": 1});
|
||||
let check = json!({"a": 1, "b": 2});
|
||||
assert!(!is_superset(&full, &check));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_nested_objects() {
|
||||
let full = json!({"a": {"b": {"c": 1, "d": 2}, "e": 3}});
|
||||
let check = json!({"a": {"b": {"c": 1}}});
|
||||
assert!(is_superset(&full, &check));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_array_subset() {
|
||||
let full = json!([1, 2, 3, 4]);
|
||||
let check = json!([2, 4]);
|
||||
assert!(is_superset(&full, &check));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_array_not_subset() {
|
||||
let full = json!([1, 2, 3]);
|
||||
let check = json!([4]);
|
||||
assert!(!is_superset(&full, &check));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_empty_check() {
|
||||
assert!(is_superset(&json!({"a": 1}), &json!({})));
|
||||
assert!(is_superset(&json!([1, 2]), &json!([])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_array_of_objects() {
|
||||
let full = json!([{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]);
|
||||
let check = json!([{"id": 1}]);
|
||||
assert!(is_superset(&full, &check));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superset_type_mismatch() {
|
||||
assert!(!is_superset(&json!(42), &json!("42")));
|
||||
assert!(!is_superset(&json!([1]), &json!(1)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,3 +156,183 @@ pub enum TriggerMode {
|
||||
Disabled,
|
||||
Suspended,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
// --- TriggerMode serde ---
|
||||
|
||||
#[test]
|
||||
fn test_trigger_mode_serialize() {
|
||||
assert_eq!(serde_json::to_value(TriggerMode::Enabled).unwrap(), json!("enabled"));
|
||||
assert_eq!(serde_json::to_value(TriggerMode::Disabled).unwrap(), json!("disabled"));
|
||||
assert_eq!(serde_json::to_value(TriggerMode::Suspended).unwrap(), json!("suspended"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_mode_deserialize() {
|
||||
let enabled: TriggerMode = serde_json::from_value(json!("enabled")).unwrap();
|
||||
assert_eq!(enabled, TriggerMode::Enabled);
|
||||
let disabled: TriggerMode = serde_json::from_value(json!("disabled")).unwrap();
|
||||
assert_eq!(disabled, TriggerMode::Disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_mode_invalid() {
|
||||
let result: Result<TriggerMode, _> = serde_json::from_value(json!("paused"));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// --- StandardTriggerQuery ---
|
||||
|
||||
#[test]
|
||||
fn test_query_default() {
|
||||
let q = StandardTriggerQuery::default();
|
||||
assert_eq!(q.offset(), 0);
|
||||
assert_eq!(q.limit(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_offset_calculation() {
|
||||
let q = StandardTriggerQuery {
|
||||
page: Some(2),
|
||||
per_page: Some(50),
|
||||
path: None,
|
||||
is_flow: None,
|
||||
path_start: None,
|
||||
};
|
||||
assert_eq!(q.offset(), 100);
|
||||
assert_eq!(q.limit(), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_offset_defaults() {
|
||||
let q = StandardTriggerQuery {
|
||||
page: None,
|
||||
per_page: None,
|
||||
path: None,
|
||||
is_flow: None,
|
||||
path_start: None,
|
||||
};
|
||||
assert_eq!(q.offset(), 0);
|
||||
assert_eq!(q.limit(), 100);
|
||||
}
|
||||
|
||||
// --- BaseTriggerData backward compatibility ---
|
||||
|
||||
#[test]
|
||||
fn test_base_trigger_data_mode_field() {
|
||||
let json = r#"{
|
||||
"path": "test",
|
||||
"script_path": "f/test/script",
|
||||
"is_flow": false,
|
||||
"mode": "enabled"
|
||||
}"#;
|
||||
let data: BaseTriggerData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(data.mode(), &TriggerMode::Enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base_trigger_data_legacy_enabled_true() {
|
||||
let json = r#"{
|
||||
"path": "test",
|
||||
"script_path": "f/test/script",
|
||||
"is_flow": false,
|
||||
"enabled": true
|
||||
}"#;
|
||||
let data: BaseTriggerData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(data.mode(), &TriggerMode::Enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base_trigger_data_legacy_enabled_false() {
|
||||
let json = r#"{
|
||||
"path": "test",
|
||||
"script_path": "f/test/script",
|
||||
"is_flow": false,
|
||||
"enabled": false
|
||||
}"#;
|
||||
let data: BaseTriggerData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(data.mode(), &TriggerMode::Disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base_trigger_data_mode_takes_precedence() {
|
||||
let json = r#"{
|
||||
"path": "test",
|
||||
"script_path": "f/test/script",
|
||||
"is_flow": false,
|
||||
"mode": "suspended",
|
||||
"enabled": true
|
||||
}"#;
|
||||
let data: BaseTriggerData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(data.mode(), &TriggerMode::Suspended);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base_trigger_data_neither_field() {
|
||||
let json = r#"{
|
||||
"path": "test",
|
||||
"script_path": "f/test/script",
|
||||
"is_flow": false
|
||||
}"#;
|
||||
let data: BaseTriggerData = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(data.mode(), &TriggerMode::Enabled);
|
||||
}
|
||||
|
||||
// --- HandlerAction ---
|
||||
|
||||
#[test]
|
||||
fn test_handler_action_serialization() {
|
||||
let action = HandlerAction::Trigger {
|
||||
path: "f/test/trigger".to_string(),
|
||||
trigger_kind: JobTriggerKind::Webhook,
|
||||
};
|
||||
let json = serde_json::to_value(&action).unwrap();
|
||||
assert_eq!(json["type"], "trigger");
|
||||
assert_eq!(json["path"], "f/test/trigger");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_action_deserialization() {
|
||||
let json = r#"{"type": "trigger", "path": "f/test/trigger", "trigger_kind": "webhook"}"#;
|
||||
let action: HandlerAction = serde_json::from_str(json).unwrap();
|
||||
match action {
|
||||
HandlerAction::Trigger { path, trigger_kind } => {
|
||||
assert_eq!(path, "f/test/trigger");
|
||||
assert_eq!(
|
||||
serde_json::to_value(&trigger_kind).unwrap(),
|
||||
serde_json::to_value(&JobTriggerKind::Webhook).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ServerState ---
|
||||
|
||||
#[test]
|
||||
fn test_server_state_skip_none_fields() {
|
||||
let state = ServerState {
|
||||
server_id: None,
|
||||
last_server_ping: None,
|
||||
error: None,
|
||||
};
|
||||
let json = serde_json::to_value(&state).unwrap();
|
||||
assert!(!json.as_object().unwrap().contains_key("server_id"));
|
||||
assert!(!json.as_object().unwrap().contains_key("error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_state_with_error() {
|
||||
let state = ServerState {
|
||||
server_id: Some("srv-1".to_string()),
|
||||
last_server_ping: None,
|
||||
error: Some("connection timeout".to_string()),
|
||||
};
|
||||
let json = serde_json::to_value(&state).unwrap();
|
||||
assert_eq!(json["server_id"], "srv-1");
|
||||
assert_eq!(json["error"], "connection timeout");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user