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:
@@ -17,13 +17,13 @@ pub async fn list_providers(
|
||||
let (count_sql, data_sql) = if enabled_filter.is_some() {
|
||||
(
|
||||
"SELECT COUNT(*) FROM providers WHERE enabled = $1",
|
||||
"SELECT id, name, display_name, base_url, api_protocol, enabled, rate_limit_rpm, rate_limit_tpm, created_at, updated_at
|
||||
"SELECT id, name, display_name, base_url, api_protocol, enabled, rate_limit_rpm, rate_limit_tpm, created_at::TEXT, updated_at::TEXT
|
||||
FROM providers WHERE enabled = $1 ORDER BY name LIMIT $2 OFFSET $3",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"SELECT COUNT(*) FROM providers",
|
||||
"SELECT id, name, display_name, base_url, api_protocol, enabled, rate_limit_rpm, rate_limit_tpm, created_at, updated_at
|
||||
"SELECT id, name, display_name, base_url, api_protocol, enabled, rate_limit_rpm, rate_limit_tpm, created_at::TEXT, updated_at::TEXT
|
||||
FROM providers ORDER BY name LIMIT $1 OFFSET $2",
|
||||
)
|
||||
};
|
||||
@@ -55,7 +55,7 @@ pub async fn list_providers(
|
||||
pub async fn get_provider(db: &PgPool, provider_id: &str) -> SaasResult<ProviderInfo> {
|
||||
let row: Option<ProviderRow> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, name, display_name, base_url, api_protocol, enabled, rate_limit_rpm, rate_limit_tpm, created_at, updated_at
|
||||
"SELECT id, name, display_name, base_url, api_protocol, enabled, rate_limit_rpm, rate_limit_tpm, created_at::TEXT, updated_at::TEXT
|
||||
FROM providers WHERE id = $1"
|
||||
)
|
||||
.bind(provider_id)
|
||||
@@ -69,7 +69,7 @@ pub async fn get_provider(db: &PgPool, provider_id: &str) -> SaasResult<Provider
|
||||
|
||||
pub async fn create_provider(db: &PgPool, req: &CreateProviderRequest, enc_key: &[u8; 32]) -> SaasResult<ProviderInfo> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// 检查名称唯一性
|
||||
let existing: Option<(String,)> = sqlx::query_as("SELECT id FROM providers WHERE name = $1")
|
||||
@@ -103,7 +103,7 @@ pub async fn create_provider(db: &PgPool, req: &CreateProviderRequest, enc_key:
|
||||
pub async fn update_provider(
|
||||
db: &PgPool, provider_id: &str, req: &UpdateProviderRequest, enc_key: &[u8; 32],
|
||||
) -> SaasResult<ProviderInfo> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// Encrypt api_key upfront if provided
|
||||
let encrypted_api_key = match req.api_key {
|
||||
@@ -160,13 +160,13 @@ pub async fn list_models(
|
||||
let (count_sql, data_sql) = if provider_id.is_some() {
|
||||
(
|
||||
"SELECT COUNT(*) FROM models WHERE provider_id = $1",
|
||||
"SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at, updated_at
|
||||
"SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT
|
||||
FROM models WHERE provider_id = $1 ORDER BY alias LIMIT $2 OFFSET $3",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"SELECT COUNT(*) FROM models",
|
||||
"SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at, updated_at
|
||||
"SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT
|
||||
FROM models ORDER BY provider_id, alias LIMIT $1 OFFSET $2",
|
||||
)
|
||||
};
|
||||
@@ -195,7 +195,7 @@ pub async fn create_model(db: &PgPool, req: &CreateModelRequest) -> SaasResult<M
|
||||
let provider = get_provider(db, &req.provider_id).await?;
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// 检查 model 唯一性
|
||||
let existing: Option<(String,)> = sqlx::query_as(
|
||||
@@ -240,7 +240,7 @@ pub async fn create_model(db: &PgPool, req: &CreateModelRequest) -> SaasResult<M
|
||||
pub async fn get_model(db: &PgPool, model_id: &str) -> SaasResult<ModelInfo> {
|
||||
let row: Option<ModelRow> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at, updated_at
|
||||
"SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT
|
||||
FROM models WHERE id = $1"
|
||||
)
|
||||
.bind(model_id)
|
||||
@@ -255,7 +255,7 @@ pub async fn get_model(db: &PgPool, model_id: &str) -> SaasResult<ModelInfo> {
|
||||
pub async fn update_model(
|
||||
db: &PgPool, model_id: &str, req: &UpdateModelRequest,
|
||||
) -> SaasResult<ModelInfo> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// COALESCE pattern: all updatable fields in a single static SQL.
|
||||
// NULL parameters leave the column unchanged.
|
||||
@@ -309,13 +309,13 @@ pub async fn list_account_api_keys(
|
||||
let (count_sql, data_sql) = if provider_id.is_some() {
|
||||
(
|
||||
"SELECT COUNT(*) FROM account_api_keys WHERE account_id = $1 AND provider_id = $2 AND revoked_at IS NULL",
|
||||
"SELECT id, provider_id, key_label, permissions, enabled, last_used_at, created_at, key_value
|
||||
"SELECT id, provider_id, key_label, permissions, enabled, last_used_at::TEXT, created_at::TEXT, key_value
|
||||
FROM account_api_keys WHERE account_id = $1 AND provider_id = $2 AND revoked_at IS NULL ORDER BY created_at DESC LIMIT $3 OFFSET $4",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"SELECT COUNT(*) FROM account_api_keys WHERE account_id = $1 AND revoked_at IS NULL",
|
||||
"SELECT id, provider_id, key_label, permissions, enabled, last_used_at, created_at, key_value
|
||||
"SELECT id, provider_id, key_label, permissions, enabled, last_used_at::TEXT, created_at::TEXT, key_value
|
||||
FROM account_api_keys WHERE account_id = $1 AND revoked_at IS NULL ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
||||
)
|
||||
};
|
||||
@@ -351,7 +351,7 @@ pub async fn create_account_api_key(
|
||||
get_provider(db, &req.provider_id).await?;
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
let permissions = serde_json::to_string(&req.permissions)?;
|
||||
|
||||
// 加密 key_value 后存储
|
||||
@@ -369,14 +369,14 @@ pub async fn create_account_api_key(
|
||||
Ok(AccountApiKeyInfo {
|
||||
id, provider_id: req.provider_id.clone(), key_label: req.key_label.clone(),
|
||||
permissions: req.permissions.clone(), enabled: true, last_used_at: None,
|
||||
created_at: now, masked_key: masked,
|
||||
created_at: now.to_rfc3339(), masked_key: masked,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn rotate_account_api_key(
|
||||
db: &PgPool, key_id: &str, account_id: &str, new_key_value: &str, enc_key: &[u8; 32],
|
||||
) -> SaasResult<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
let encrypted_value = crypto::encrypt_value(new_key_value, enc_key)?;
|
||||
let result = sqlx::query(
|
||||
"UPDATE account_api_keys SET key_value = $1, updated_at = $2 WHERE id = $3 AND account_id = $4 AND revoked_at IS NULL"
|
||||
@@ -393,7 +393,7 @@ pub async fn rotate_account_api_key(
|
||||
pub async fn revoke_account_api_key(
|
||||
db: &PgPool, key_id: &str, account_id: &str,
|
||||
) -> SaasResult<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
let result = sqlx::query(
|
||||
"UPDATE account_api_keys SET revoked_at = $1 WHERE id = $2 AND account_id = $3 AND revoked_at IS NULL"
|
||||
)
|
||||
@@ -448,8 +448,7 @@ pub async fn get_usage_stats(
|
||||
let from_days = (chrono::Utc::now() - chrono::Duration::days(days))
|
||||
.date_naive()
|
||||
.and_hms_opt(0, 0, 0).unwrap()
|
||||
.and_utc()
|
||||
.to_rfc3339();
|
||||
.and_utc();
|
||||
let daily_sql = "SELECT created_at::date::text as day, COUNT(*)::bigint AS request_count, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens
|
||||
FROM usage_records WHERE account_id = $1 AND created_at >= $2
|
||||
GROUP BY created_at::date ORDER BY day DESC LIMIT $3";
|
||||
@@ -480,7 +479,7 @@ pub async fn record_usage(
|
||||
input_tokens: i64, output_tokens: i64, latency_ms: Option<i64>,
|
||||
status: &str, error_message: Option<&str>,
|
||||
) -> SaasResult<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query(
|
||||
"INSERT INTO usage_records (account_id, provider_id, model_id, input_tokens, output_tokens, latency_ms, status, error_message, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"
|
||||
@@ -506,7 +505,7 @@ fn mask_api_key(key: &str) -> String {
|
||||
pub async fn list_model_groups(db: &PgPool) -> SaasResult<Vec<ModelGroupInfo>> {
|
||||
let group_rows: Vec<(String, String, String, String, bool, String, String, String)> = sqlx::query_as(
|
||||
"SELECT id, name, display_name, COALESCE(description, ''), enabled,
|
||||
COALESCE(failover_strategy, 'quota_aware'), created_at, updated_at
|
||||
COALESCE(failover_strategy, 'quota_aware'), created_at::TEXT, updated_at::TEXT
|
||||
FROM model_groups ORDER BY name"
|
||||
).fetch_all(db).await?;
|
||||
|
||||
@@ -535,7 +534,7 @@ pub async fn list_model_groups(db: &PgPool) -> SaasResult<Vec<ModelGroupInfo>> {
|
||||
pub async fn get_model_group(db: &PgPool, group_id: &str) -> SaasResult<ModelGroupInfo> {
|
||||
let row: Option<(String, String, String, String, bool, String, String, String)> = sqlx::query_as(
|
||||
"SELECT id, name, display_name, COALESCE(description, ''), enabled,
|
||||
COALESCE(failover_strategy, 'quota_aware'), created_at, updated_at
|
||||
COALESCE(failover_strategy, 'quota_aware'), created_at::TEXT, updated_at::TEXT
|
||||
FROM model_groups WHERE id = $1"
|
||||
).bind(group_id).fetch_optional(db).await?;
|
||||
|
||||
@@ -566,7 +565,7 @@ pub async fn create_model_group(db: &PgPool, req: &CreateModelGroupRequest) -> S
|
||||
}
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// 检查名称唯一性
|
||||
let existing: Option<(String,)> = sqlx::query_as("SELECT id FROM model_groups WHERE name = $1")
|
||||
@@ -598,7 +597,7 @@ pub async fn create_model_group(db: &PgPool, req: &CreateModelGroupRequest) -> S
|
||||
pub async fn update_model_group(
|
||||
db: &PgPool, group_id: &str, req: &UpdateModelGroupRequest,
|
||||
) -> SaasResult<ModelGroupInfo> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE model_groups SET
|
||||
|
||||
Reference in New Issue
Block a user