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
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:
@@ -38,7 +38,7 @@ pub struct KeySelection {
|
||||
///
|
||||
/// 优化: 单次 JOIN 查询获取 Key + 当前分钟使用量,避免 N+1 查询
|
||||
pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32]) -> SaasResult<KeySelection> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
let current_minute = chrono::Utc::now().format("%Y-%m-%dT%H:%M").to_string();
|
||||
|
||||
// 单次查询: 活跃 Key + 当前分钟的 RPM/TPM 使用量 (LEFT JOIN)
|
||||
@@ -94,14 +94,14 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
if rows.is_empty() {
|
||||
// 检查是否有冷却中的 Key,返回预计等待时间
|
||||
let cooldown_row: Option<(String,)> = sqlx::query_as(
|
||||
"SELECT cooldown_until FROM provider_keys
|
||||
"SELECT cooldown_until::TEXT FROM provider_keys
|
||||
WHERE provider_id = $1 AND is_active = TRUE AND cooldown_until IS NOT NULL AND cooldown_until > $2
|
||||
ORDER BY cooldown_until ASC
|
||||
LIMIT 1"
|
||||
).bind(provider_id).bind(&now).fetch_optional(db).await?;
|
||||
|
||||
if let Some((earliest,)) = cooldown_row {
|
||||
let wait_secs = parse_cooldown_remaining(&earliest, &now);
|
||||
let wait_secs = parse_cooldown_remaining(&earliest, &now.to_rfc3339());
|
||||
return Err(SaasError::RateLimited(
|
||||
format!("所有 Key 均在冷却中,预计 {} 秒后可用", wait_secs)
|
||||
));
|
||||
@@ -178,13 +178,13 @@ pub async fn mark_key_429(
|
||||
retry_after_seconds: Option<u64>,
|
||||
) -> SaasResult<()> {
|
||||
let cooldown = if let Some(secs) = retry_after_seconds {
|
||||
(chrono::Utc::now() + chrono::Duration::seconds(secs as i64)).to_rfc3339()
|
||||
(chrono::Utc::now() + chrono::Duration::seconds(secs as i64))
|
||||
} else {
|
||||
// 默认 5 分钟冷却
|
||||
(chrono::Utc::now() + chrono::Duration::minutes(5)).to_rfc3339()
|
||||
(chrono::Utc::now() + chrono::Duration::minutes(5))
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE provider_keys SET last_429_at = $1, cooldown_until = $2, updated_at = $3
|
||||
@@ -210,7 +210,7 @@ pub async fn list_provider_keys(
|
||||
let rows: Vec<ProviderKeyRow> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, provider_id, key_label, priority, max_rpm, max_tpm, is_active,
|
||||
last_429_at, cooldown_until, total_requests, total_tokens, created_at, updated_at
|
||||
last_429_at::TEXT, cooldown_until::TEXT, total_requests, total_tokens, created_at::TEXT, updated_at::TEXT
|
||||
FROM provider_keys WHERE provider_id = $1 ORDER BY priority ASC"
|
||||
).bind(provider_id).fetch_all(db).await?;
|
||||
|
||||
@@ -244,7 +244,7 @@ pub async fn add_provider_key(
|
||||
max_tpm: Option<i64>,
|
||||
) -> SaasResult<String> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO provider_keys (id, provider_id, key_label, key_value, priority, max_rpm, max_tpm, is_active, total_requests, total_tokens, created_at, updated_at)
|
||||
@@ -264,7 +264,7 @@ pub async fn toggle_key_active(
|
||||
key_id: &str,
|
||||
active: bool,
|
||||
) -> SaasResult<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query(
|
||||
"UPDATE provider_keys SET is_active = $1, updated_at = $2 WHERE id = $3"
|
||||
).bind(active).bind(&now).bind(key_id).execute(db).await?;
|
||||
|
||||
@@ -48,14 +48,14 @@ pub async fn create_relay_task(
|
||||
max_attempts: u32,
|
||||
) -> SaasResult<RelayTaskInfo> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
let request_hash = hash_request(request_body);
|
||||
let max_attempts = max_attempts.max(1).min(5);
|
||||
|
||||
let query = sqlx::query_as::<_, RelayTaskRow>(
|
||||
"INSERT INTO relay_tasks (id, account_id, provider_id, model_id, request_hash, request_body, status, priority, attempt_count, max_attempts, queued_at, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'queued', $7, 0, $8, $9, $9)
|
||||
RETURNING id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at, started_at, completed_at, created_at"
|
||||
RETURNING id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at::TEXT, started_at::TEXT, completed_at::TEXT, created_at::TEXT"
|
||||
)
|
||||
.bind(&id).bind(account_id).bind(provider_id).bind(model_id)
|
||||
.bind(&request_hash).bind(request_body).bind(priority).bind(max_attempts as i64).bind(&now);
|
||||
@@ -69,7 +69,7 @@ pub async fn create_relay_task(
|
||||
sqlx::query_as::<_, RelayTaskRow>(
|
||||
"INSERT INTO relay_tasks (id, account_id, provider_id, model_id, request_hash, request_body, status, priority, attempt_count, max_attempts, queued_at, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'queued', $7, 0, $8, $9, $9)
|
||||
RETURNING id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at, started_at, completed_at, created_at"
|
||||
RETURNING id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at::TEXT, started_at::TEXT, completed_at::TEXT, created_at::TEXT"
|
||||
)
|
||||
.bind(&id).bind(account_id).bind(provider_id).bind(model_id)
|
||||
.bind(&request_hash).bind(request_body).bind(priority).bind(max_attempts as i64).bind(&now)
|
||||
@@ -91,7 +91,7 @@ pub async fn create_relay_task(
|
||||
pub async fn get_relay_task(db: &PgPool, task_id: &str) -> SaasResult<RelayTaskInfo> {
|
||||
let row: Option<RelayTaskRow> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at, started_at, completed_at, created_at
|
||||
"SELECT id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at::TEXT, started_at::TEXT, completed_at::TEXT, created_at::TEXT
|
||||
FROM relay_tasks WHERE id = $1"
|
||||
)
|
||||
.bind(task_id)
|
||||
@@ -117,13 +117,13 @@ pub async fn list_relay_tasks(
|
||||
let (count_sql, data_sql) = if query.status.is_some() {
|
||||
(
|
||||
"SELECT COUNT(*) FROM relay_tasks WHERE account_id = $1 AND status = $2",
|
||||
"SELECT id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at, started_at, completed_at, created_at
|
||||
"SELECT id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at::TEXT, started_at::TEXT, completed_at::TEXT, created_at::TEXT
|
||||
FROM relay_tasks WHERE account_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"SELECT COUNT(*) FROM relay_tasks WHERE account_id = $1",
|
||||
"SELECT id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at, started_at, completed_at, created_at
|
||||
"SELECT id, account_id, provider_id, model_id, status, priority, attempt_count, max_attempts, input_tokens, output_tokens, error_message, queued_at::TEXT, started_at::TEXT, completed_at::TEXT, created_at::TEXT
|
||||
FROM relay_tasks WHERE account_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||
)
|
||||
};
|
||||
@@ -154,7 +154,7 @@ pub async fn update_task_status(
|
||||
input_tokens: Option<i64>, output_tokens: Option<i64>,
|
||||
error_message: Option<&str>,
|
||||
) -> SaasResult<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
match status {
|
||||
"processing" => {
|
||||
|
||||
Reference in New Issue
Block a user