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
- 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>
320 lines
11 KiB
Rust
320 lines
11 KiB
Rust
//! 定时任务模块集成测试
|
||
//!
|
||
//! 覆盖 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");
|
||
}
|