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