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

@@ -4,8 +4,16 @@ use sqlx::{PgPool, Row};
use crate::error::{SaasError, SaasResult};
use super::types::*;
/// Shared SELECT column list.
/// Shared SELECT column list (with ::TEXT cast for TIMESTAMPTZ decode).
const SELECT_COLUMNS: &str = "\
id, name, description, category, source, model, system_prompt, \
tools, capabilities, temperature, max_tokens, visibility, status, \
current_version, created_at::TEXT, updated_at::TEXT, \
soul_content, scenarios, welcome_message, quick_commands, \
personality, communication_style, emoji, version, source_id";
/// Plain column names for INSERT statements (no casts).
const INSERT_COLUMNS: &str = "\
id, name, description, category, source, model, system_prompt, \
tools, capabilities, temperature, max_tokens, visibility, status, \
current_version, created_at, updated_at, \
@@ -69,14 +77,14 @@ pub async fn create_template(
source_id: Option<&str>,
) -> SaasResult<AgentTemplateInfo> {
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now().to_rfc3339();
let now = chrono::Utc::now();
let tools_json = serde_json::to_string(tools).unwrap_or_else(|_| "[]".to_string());
let caps_json = serde_json::to_string(capabilities).unwrap_or_else(|_| "[]".to_string());
let scenarios_json = serde_json::to_string(&scenarios.unwrap_or(&[])).unwrap_or_else(|_| "[]".to_string());
let quick_commands_json = serde_json::to_string(&quick_commands.unwrap_or(&[])).unwrap_or_else(|_| "[]".to_string());
sqlx::query(
&format!("INSERT INTO agent_templates ({}) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'active',1,$13,$13,$14,$15,$16,$17,$18,$19,$20,1,$21)", SELECT_COLUMNS)
&format!("INSERT INTO agent_templates ({}) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'active',1,$13,$13,$14,$15,$16,$17,$18,$19,$20,1,$21)", INSERT_COLUMNS)
)
.bind(&id) // $1 id
.bind(name) // $2 name
@@ -209,7 +217,7 @@ pub async fn update_template(
// Confirm existence
get_template(db, id).await?;
let now = chrono::Utc::now().to_rfc3339();
let now = chrono::Utc::now();
// Serialize JSON fields upfront so we can bind Option<&str> consistently
let tools_json = tools.map(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()));
@@ -282,7 +290,7 @@ pub async fn assign_template_to_account(
return Err(SaasError::InvalidInput("模板不可用(已归档)".into()));
}
let now = chrono::Utc::now().to_rfc3339();
let now = chrono::Utc::now();
sqlx::query(
"UPDATE accounts SET assigned_template_id = $1, updated_at = $2 WHERE id = $3"
)
@@ -317,7 +325,7 @@ pub async fn get_assigned_template(
Ok(t) => Ok(Some(t)),
Err(SaasError::NotFound(_)) => {
// Template deleted — clear stale reference
let now = chrono::Utc::now().to_rfc3339();
let now = chrono::Utc::now();
sqlx::query(
"UPDATE accounts SET assigned_template_id = NULL, updated_at = $1 WHERE id = $2"
)
@@ -336,7 +344,7 @@ pub async fn unassign_template(
db: &PgPool,
account_id: &str,
) -> SaasResult<()> {
let now = chrono::Utc::now().to_rfc3339();
let now = chrono::Utc::now();
sqlx::query(
"UPDATE accounts SET assigned_template_id = NULL, updated_at = $1 WHERE id = $2"
)