test(saas): Phase 1 integration tests — billing + scheduled_task + knowledge (68 tests)
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>
This commit is contained in:
iven
2026-04-07 14:25:34 +08:00
parent a5b887051d
commit 7de486bfca
27 changed files with 1317 additions and 187 deletions

View File

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