mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
fix(public): 修复用户可见性、额度、验证与 Codex 探测
This commit is contained in:
@@ -7,6 +7,40 @@ use super::{
|
||||
StoredAuthApiKeyExportRecord, StoredAuthApiKeySnapshot, StoredUserAuthRecord,
|
||||
StoredUserExportRow, Utc,
|
||||
};
|
||||
use aether_data_contracts::repository::billing::{
|
||||
BillingReadRepository, StoredBillingModelContext, UserDailyQuotaAvailabilityRecord,
|
||||
};
|
||||
use aether_data_contracts::DataLayerError;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StaticDailyQuotaBillingRepository {
|
||||
user_id: String,
|
||||
quota: UserDailyQuotaAvailabilityRecord,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl BillingReadRepository for StaticDailyQuotaBillingRepository {
|
||||
async fn find_model_context(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
provider_api_key_id: Option<&str>,
|
||||
global_model_name: &str,
|
||||
) -> Result<Option<StoredBillingModelContext>, DataLayerError> {
|
||||
let _ = (provider_id, provider_api_key_id, global_model_name);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn find_user_daily_quota_availability(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<UserDailyQuotaAvailabilityRecord>, DataLayerError> {
|
||||
if user_id == self.user_id {
|
||||
Ok(Some(self.quota.clone()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stable_dashboard_now() -> chrono::DateTime<Utc> {
|
||||
Utc::now()
|
||||
@@ -173,6 +207,93 @@ async fn gateway_handles_dashboard_stats_locally_without_proxying_upstream() {
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_dashboard_stats_user_wallet_card_uses_wallet_center_balance_breakdown() {
|
||||
let now = stable_dashboard_now();
|
||||
let user = sample_auth_user(now);
|
||||
let access_token = build_test_auth_token(
|
||||
"access",
|
||||
serde_json::Map::from_iter([
|
||||
("user_id".to_string(), json!(user.id)),
|
||||
("role".to_string(), json!(user.role)),
|
||||
(
|
||||
"created_at".to_string(),
|
||||
json!(user.created_at.map(|value| value.to_rfc3339())),
|
||||
),
|
||||
("session_id".to_string(), json!("session-dashboard-wallet")),
|
||||
]),
|
||||
chrono::Utc::now() + chrono::Duration::hours(1),
|
||||
);
|
||||
let session = sample_auth_session(
|
||||
"user-auth-1",
|
||||
"session-dashboard-wallet",
|
||||
"device-dashboard-wallet",
|
||||
"refresh-dashboard-wallet",
|
||||
now,
|
||||
);
|
||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![]));
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
|
||||
user.clone()
|
||||
]));
|
||||
let mut wallet = sample_auth_wallet("user-auth-1", now);
|
||||
wallet.balance = 7.0;
|
||||
wallet.gift_balance = 3.0;
|
||||
let wallet_repository = Arc::new(InMemoryWalletRepository::seed(vec![wallet]));
|
||||
let billing_repository: Arc<dyn BillingReadRepository> =
|
||||
Arc::new(StaticDailyQuotaBillingRepository {
|
||||
user_id: "user-auth-1".to_string(),
|
||||
quota: UserDailyQuotaAvailabilityRecord {
|
||||
has_active_daily_quota: true,
|
||||
total_quota_usd: 120.0,
|
||||
used_usd: 20.0,
|
||||
remaining_usd: 100.0,
|
||||
allow_wallet_overage: true,
|
||||
},
|
||||
});
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(Vec::<(
|
||||
Option<String>,
|
||||
StoredAuthApiKeySnapshot,
|
||||
)>::new()));
|
||||
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_gateway_with_builder(|| {
|
||||
let data_state = GatewayDataState::with_usage_billing_and_wallet_for_tests(
|
||||
usage_repository,
|
||||
Arc::clone(&billing_repository),
|
||||
wallet_repository,
|
||||
)
|
||||
.with_user_reader(user_repository)
|
||||
.with_auth_api_key_reader(auth_repository);
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_auth_sessions_for_tests([session])
|
||||
})
|
||||
.await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!("{gateway_url}/api/dashboard/stats?days=30"))
|
||||
.header("authorization", format!("Bearer {access_token}"))
|
||||
.header("x-client-device-id", "device-dashboard-wallet")
|
||||
.header("user-agent", "AetherTest/1.0")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["stats"][2]["name"], "钱包余额");
|
||||
assert_eq!(payload["stats"][2]["value"], "$110.00");
|
||||
assert_eq!(
|
||||
payload["stats"][2]["subValue"],
|
||||
"套餐额度 $100.00 · 钱包余额 $10.00"
|
||||
);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_dashboard_stats_include_end_of_day_boundary() {
|
||||
let now = stable_dashboard_now();
|
||||
@@ -1071,16 +1192,9 @@ async fn gateway_handles_dashboard_provider_status_locally_without_proxying_upst
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
let providers = payload["providers"].as_array().expect("array");
|
||||
assert_eq!(providers.len(), 3);
|
||||
assert_eq!(providers[0]["name"], "openai");
|
||||
assert_eq!(providers[0]["requests"], 2);
|
||||
assert_eq!(providers[1]["name"], "claude");
|
||||
assert_eq!(providers[1]["requests"], 1);
|
||||
assert_eq!(providers[2]["name"], "gemini");
|
||||
assert_eq!(providers[2]["requests"], 0);
|
||||
assert_eq!(payload["detail"], "仅管理员可查看供应商状态");
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
Reference in New Issue
Block a user