Files
zclaw_openfang/crates/zclaw-saas/tests/scheduled_task_test.rs
iven 7de486bfca
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
test(saas): Phase 1 integration tests — billing + scheduled_task + knowledge (68 tests)
- Fix TIMESTAMPTZ decode errors: add ::TEXT cast to all SELECT queries
  where Row structs use String for TIMESTAMPTZ columns (~22 locations)
- Fix Axum 0.7 route params: {id} → :id in billing/knowledge/scheduled_task routes
- Fix JSONB bind: scheduled_task INSERT uses ::jsonb cast for input_payload
- Add billing_test.rs (14 tests): plans, subscription, usage, payments, invoices
- Add scheduled_task_test.rs (12 tests): CRUD, validation, isolation
- Add knowledge_test.rs (20 tests): categories, items, versions, search, analytics, permissions
- Fix auth test regression: 6 tests were failing due to TIMESTAMPTZ type mismatch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 14:25:34 +08:00

320 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

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

//! 定时任务模块集成测试
//!
//! 覆盖 scheduled_task 模块的 CRUD 端点5 端点)。
mod common;
use common::*;
use axum::http::StatusCode;
/// 创建 cron 类型任务的请求体
fn cron_task_body(name: &str) -> serde_json::Value {
serde_json::json!({
"name": name,
"schedule": "0 8 * * *",
"schedule_type": "cron",
"target": {
"type": "agent",
"id": "test-agent-1"
},
"description": "测试定时任务",
"enabled": true
})
}
/// 创建 interval 类型任务的请求体
fn interval_task_body(name: &str) -> serde_json::Value {
serde_json::json!({
"name": name,
"schedule": "30m",
"schedule_type": "interval",
"target": {
"type": "hand",
"id": "collector"
}
})
}
// ── 创建任务 ───────────────────────────────────────────────────
#[tokio::test]
async fn test_create_cron_task() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_cron_user").await;
let (status, body) = send(&app, post(
"/api/v1/scheduler/tasks",
&token,
cron_task_body("每日早报"),
)).await;
assert_eq!(status, StatusCode::CREATED, "create cron task failed: {body}");
assert!(body["id"].is_string(), "missing id");
assert_eq!(body["name"], "每日早报");
assert_eq!(body["schedule"], "0 8 * * *");
assert_eq!(body["schedule_type"], "cron");
assert_eq!(body["target"]["type"], "agent");
assert_eq!(body["target"]["id"], "test-agent-1");
assert_eq!(body["enabled"], true);
assert!(body["created_at"].is_string());
assert_eq!(body["run_count"], 0);
}
#[tokio::test]
async fn test_create_interval_task() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_interval_user").await;
let (status, body) = send(&app, post(
"/api/v1/scheduler/tasks",
&token,
interval_task_body("定时采集"),
)).await;
assert_eq!(status, StatusCode::CREATED, "create interval task failed: {body}");
assert_eq!(body["schedule_type"], "interval");
assert_eq!(body["schedule"], "30m");
assert_eq!(body["target"]["type"], "hand");
}
#[tokio::test]
async fn test_create_once_task() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_once_user").await;
let body = serde_json::json!({
"name": "一次性任务",
"schedule": "2026-12-31T00:00:00Z",
"schedule_type": "once",
"target": {
"type": "workflow",
"id": "wf-1"
}
});
let (status, resp) = send(&app, post("/api/v1/scheduler/tasks", &token, body)).await;
assert_eq!(status, StatusCode::CREATED, "create once task failed: {resp}");
assert_eq!(resp["schedule_type"], "once");
assert_eq!(resp["target"]["type"], "workflow");
}
#[tokio::test]
async fn test_create_task_validation() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_valid_user").await;
// 空名称
let (status, _) = send(&app, post("/api/v1/scheduler/tasks", &token,
serde_json::json!({
"name": "",
"schedule": "0 * * * *",
"schedule_type": "cron",
"target": { "type": "agent", "id": "a1" }
})
)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "should reject empty name");
// 空 schedule
let (status, _) = send(&app, post("/api/v1/scheduler/tasks", &token,
serde_json::json!({
"name": "valid",
"schedule": "",
"schedule_type": "cron",
"target": { "type": "agent", "id": "a1" }
})
)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "should reject empty schedule");
// 无效 schedule_type
let (status, _) = send(&app, post("/api/v1/scheduler/tasks", &token,
serde_json::json!({
"name": "valid",
"schedule": "0 * * * *",
"schedule_type": "invalid",
"target": { "type": "agent", "id": "a1" }
})
)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "should reject invalid schedule_type");
// 无效 target_type
let (status, _) = send(&app, post("/api/v1/scheduler/tasks", &token,
serde_json::json!({
"name": "valid",
"schedule": "0 * * * *",
"schedule_type": "cron",
"target": { "type": "invalid_type", "id": "a1" }
})
)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "should reject invalid target_type");
}
// ── 列出任务 ───────────────────────────────────────────────────
#[tokio::test]
async fn test_list_tasks() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_list_user").await;
// 创建 2 个任务
let _ = send(&app, post("/api/v1/scheduler/tasks", &token,
cron_task_body("任务A")
)).await;
let _ = send(&app, post("/api/v1/scheduler/tasks", &token,
interval_task_body("任务B")
)).await;
let (status, body) = send(&app, get("/api/v1/scheduler/tasks", &token)).await;
assert_eq!(status, StatusCode::OK, "list_tasks failed: {body}");
let arr = body.as_array().expect("should be array");
assert_eq!(arr.len(), 2, "expected 2 tasks, got {}", arr.len());
}
#[tokio::test]
async fn test_list_tasks_isolation() {
let (app, _pool) = build_test_app().await;
let token_a = register_token(&app, "sched_iso_user_a").await;
let token_b = register_token(&app, "sched_iso_user_b").await;
// 用户 A 创建任务
let _ = send(&app, post("/api/v1/scheduler/tasks", &token_a,
cron_task_body("A的任务")
)).await;
// 用户 B 创建任务
let _ = send(&app, post("/api/v1/scheduler/tasks", &token_b,
cron_task_body("B的任务")
)).await;
// 用户 A 只能看到自己的任务
let (_, body_a) = send(&app, get("/api/v1/scheduler/tasks", &token_a)).await;
let arr_a = body_a.as_array().unwrap();
assert_eq!(arr_a.len(), 1, "user A should see 1 task");
assert_eq!(arr_a[0]["name"], "A的任务");
// 用户 B 只能看到自己的任务
let (_, body_b) = send(&app, get("/api/v1/scheduler/tasks", &token_b)).await;
let arr_b = body_b.as_array().unwrap();
assert_eq!(arr_b.len(), 1, "user B should see 1 task");
assert_eq!(arr_b[0]["name"], "B的任务");
}
// ── 获取单个任务 ───────────────────────────────────────────────
#[tokio::test]
async fn test_get_task() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_get_user").await;
let (_, create_body) = send(&app, post("/api/v1/scheduler/tasks", &token,
cron_task_body("获取测试")
)).await;
let task_id = create_body["id"].as_str().unwrap();
let (status, body) = send(&app, get(
&format!("/api/v1/scheduler/tasks/{}", task_id),
&token,
)).await;
assert_eq!(status, StatusCode::OK, "get_task failed: {body}");
assert_eq!(body["id"], task_id);
assert_eq!(body["name"], "获取测试");
}
#[tokio::test]
async fn test_get_task_not_found() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_404_user").await;
let (status, _) = send(&app, get(
"/api/v1/scheduler/tasks/nonexistent-id",
&token,
)).await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_get_task_wrong_account() {
let (app, _pool) = build_test_app().await;
let token_a = register_token(&app, "sched_wa_user_a").await;
let token_b = register_token(&app, "sched_wa_user_b").await;
// 用户 A 创建任务
let (_, create_body) = send(&app, post("/api/v1/scheduler/tasks", &token_a,
cron_task_body("A私有任务")
)).await;
let task_id = create_body["id"].as_str().unwrap();
// 用户 B 不应看到用户 A 的任务
let (status, _) = send(&app, get(
&format!("/api/v1/scheduler/tasks/{}", task_id),
&token_b,
)).await;
assert_eq!(status, StatusCode::NOT_FOUND, "should not see other user's task");
}
// ── 更新任务 ───────────────────────────────────────────────────
#[tokio::test]
async fn test_update_task() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_update_user").await;
let (_, create_body) = send(&app, post("/api/v1/scheduler/tasks", &token,
cron_task_body("原始名称")
)).await;
let task_id = create_body["id"].as_str().unwrap();
let (status, body) = send(&app, patch(
&format!("/api/v1/scheduler/tasks/{}", task_id),
&token,
serde_json::json!({
"name": "更新后名称",
"enabled": false,
"description": "已禁用"
}),
)).await;
assert_eq!(status, StatusCode::OK, "update_task failed: {body}");
assert_eq!(body["name"], "更新后名称");
assert_eq!(body["enabled"], false);
assert_eq!(body["description"], "已禁用");
// 未更新的字段应保持不变
assert_eq!(body["schedule"], "0 8 * * *");
}
// ── 删除任务 ───────────────────────────────────────────────────
#[tokio::test]
async fn test_delete_task() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_del_user").await;
let (_, create_body) = send(&app, post("/api/v1/scheduler/tasks", &token,
cron_task_body("待删除任务")
)).await;
let task_id = create_body["id"].as_str().unwrap();
let (status, _) = send(&app, delete(
&format!("/api/v1/scheduler/tasks/{}", task_id),
&token,
)).await;
assert_eq!(status, StatusCode::NO_CONTENT, "delete should return 204");
// 确认已删除
let (status, _) = send(&app, get(
&format!("/api/v1/scheduler/tasks/{}", task_id),
&token,
)).await;
assert_eq!(status, StatusCode::NOT_FOUND, "deleted task should be 404");
}
#[tokio::test]
async fn test_delete_task_not_found() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "sched_del404_user").await;
let (status, _) = send(&app, delete(
"/api/v1/scheduler/tasks/nonexistent-id",
&token,
)).await;
assert_eq!(status, StatusCode::NOT_FOUND, "delete nonexistent should return 404");
}