Merge remote-tracking branch 'upstream/aether-rust-pioneer' into fix-management-token-oauth-jsonb

# Conflicts:
#	crates/aether-data/src/lifecycle/bootstrap/postgres.rs
#	crates/aether-data/src/lifecycle/migrate/tests.rs
This commit is contained in:
Entropy.Xu
2026-05-10 18:36:53 +08:00
109 changed files with 11022 additions and 948 deletions

View File

@@ -13,7 +13,9 @@ use aether_data::repository::global_models::InMemoryGlobalModelReadRepository;
use aether_data::repository::oauth_providers::InMemoryOAuthProviderRepository;
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data::repository::proxy_nodes::InMemoryProxyNodeRepository;
use aether_data::repository::users::{InMemoryUserReadRepository, StoredUserExportRow};
use aether_data::repository::users::{
InMemoryUserReadRepository, StoredUserAuthRecord, UpsertUserGroupRecord, UserReadRepository,
};
use aether_data::repository::wallet::{InMemoryWalletRepository, StoredWalletSnapshot};
use aether_data_contracts::repository::global_models::StoredPublicGlobalModel;
use axum::body::Body;
@@ -596,8 +598,8 @@ async fn gateway_handles_admin_system_users_export_locally_with_trusted_admin_pr
}),
);
let user_repository = Arc::new(InMemoryUserReadRepository::seed_export_users(vec![
StoredUserExportRow::new(
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
StoredUserAuthRecord::new(
"user-1".to_string(),
Some("alice@example.com".to_string()),
true,
@@ -608,12 +610,40 @@ async fn gateway_handles_admin_system_users_export_locally_with_trusted_admin_pr
Some(json!(["openai"])),
Some(json!(["openai:chat"])),
Some(json!(["gpt-5"])),
Some(120),
Some(json!({"gpt-5": {"cache_1h": true}})),
true,
false,
Some(chrono::Utc::now()),
None,
)
.expect("user export row should build"),
.expect("user export row should build")
.with_policy_modes(
"specific".to_string(),
"specific".to_string(),
"specific".to_string(),
)
.expect("user policy modes should build"),
]));
let user_group = user_repository
.create_user_group(UpsertUserGroupRecord {
name: "Restricted GPT".to_string(),
description: Some("GPT-only users".to_string()),
priority: 10,
allowed_providers: Some(vec!["openai".to_string()]),
allowed_providers_mode: "specific".to_string(),
allowed_api_formats: Some(vec!["openai:chat".to_string()]),
allowed_api_formats_mode: "specific".to_string(),
allowed_models: Some(vec!["gpt-5".to_string()]),
allowed_models_mode: "specific".to_string(),
rate_limit: Some(60),
rate_limit_mode: "custom".to_string(),
})
.await
.expect("user group should create")
.expect("user group should exist");
user_repository
.replace_user_groups_for_user("user-1", std::slice::from_ref(&user_group.id))
.await
.expect("user group membership should create");
let auth_repository = Arc::new(
InMemoryAuthApiKeySnapshotRepository::default().with_export_records(vec![
StoredAuthApiKeyExportRecord::new(
@@ -728,9 +758,24 @@ async fn gateway_handles_admin_system_users_export_locally_with_trusted_admin_pr
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["version"], "1.3");
assert_eq!(payload["version"], "1.4");
assert!(payload["exported_at"].as_str().is_some());
assert_eq!(payload["user_groups"][0]["name"], "Restricted GPT");
assert!(payload["user_groups"][0].get("priority").is_none());
assert_eq!(
payload["user_groups"][0]["allowed_models"],
json!(["gpt-5"])
);
assert_eq!(payload["users"][0]["email"], "alice@example.com");
assert_eq!(
payload["users"][0]["allowed_models_mode"],
json!("specific")
);
assert_eq!(payload["users"][0]["rate_limit_mode"], json!("system"));
assert_eq!(
payload["users"][0]["group_names"],
json!(["Restricted GPT"])
);
assert_eq!(payload["users"][0]["wallet"]["balance"], json!(12.5));
assert_eq!(
payload["users"][0]["wallet"]["recharge_balance"],

View File

@@ -12,7 +12,7 @@ use aether_data::repository::oauth_providers::{
InMemoryOAuthProviderRepository, OAuthProviderReadRepository, StoredOAuthProviderConfig,
};
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data::repository::users::StoredUserAuthRecord;
use aether_data::repository::users::{StoredUserAuthRecord, UserReadRepository};
use aether_data::repository::wallet::{StoredWalletSnapshot, WalletLookupKey};
use aether_data_contracts::repository::global_models::{
AdminGlobalModelListQuery, AdminProviderModelListQuery, GlobalModelReadRepository,
@@ -556,11 +556,14 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
}));
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::default());
let user_repository =
Arc::new(aether_data::repository::users::InMemoryUserReadRepository::default());
let (upstream_url, upstream_handle) = start_server(upstream).await;
let state = AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(
GatewayDataState::with_auth_api_key_repository_for_tests(Arc::clone(&auth_repository))
.with_user_reader(user_repository)
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
)
.with_auth_users_for_tests([sample_import_admin_user("admin-user-123")])
@@ -575,8 +578,21 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"version": "1.3",
"version": "1.4",
"merge_mode": "overwrite",
"user_groups": [{
"id": "source-group-1",
"name": "GPT Import",
"description": "Imported group",
"allowed_providers": ["openai"],
"allowed_providers_mode": "specific",
"allowed_api_formats": ["openai:chat"],
"allowed_api_formats_mode": "specific",
"allowed_models": ["gpt-5"],
"allowed_models_mode": "specific",
"rate_limit": 44,
"rate_limit_mode": "custom"
}],
"users": [{
"email": "alice@example.com",
"email_verified": true,
@@ -587,6 +603,10 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
"allowed_api_formats": ["openai:chat"],
"allowed_models": ["gpt-5"],
"rate_limit": 77,
"allowed_models_mode": "specific",
"rate_limit_mode": "custom",
"group_ids": ["source-group-1"],
"group_names": ["GPT Import"],
"is_active": true,
"wallet": {
"balance": 20.0,
@@ -654,6 +674,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
let payload: Value = response.json().await.expect("json body should parse");
assert_eq!(status, StatusCode::OK, "payload={payload}");
assert_eq!(payload["message"], "用户数据导入成功");
assert_eq!(payload["stats"]["user_groups"]["created"], json!(1));
assert_eq!(payload["stats"]["users"]["created"], json!(1));
assert_eq!(payload["stats"]["api_keys"]["created"], json!(1));
assert_eq!(payload["stats"]["standalone_keys"]["created"], json!(1));
@@ -683,8 +704,22 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
imported_user.allowed_models,
Some(vec!["gpt-5".to_string()])
);
assert_eq!(imported_user.allowed_models_mode, "specific");
assert!(imported_user.is_active);
let imported_groups = state
.list_user_groups_for_user(&imported_user.id)
.await
.expect("user groups should load");
assert_eq!(imported_groups.len(), 1);
assert_eq!(imported_groups[0].name, "GPT Import");
assert_eq!(imported_groups[0].allowed_models_mode, "specific");
assert_eq!(
imported_groups[0].allowed_models,
Some(vec!["gpt-5".to_string()])
);
assert_eq!(imported_groups[0].rate_limit, Some(44));
let user_wallet = state
.find_wallet(WalletLookupKey::UserId(&imported_user.id))
.await

View File

@@ -2927,8 +2927,11 @@ async fn gateway_handles_admin_usage_cache_affinity_interval_timeline_with_legac
role: "user".to_string(),
auth_source: "local".to_string(),
allowed_providers: None,
allowed_providers_mode: "unrestricted".to_string(),
allowed_api_formats: None,
allowed_api_formats_mode: "unrestricted".to_string(),
allowed_models: None,
allowed_models_mode: "unrestricted".to_string(),
is_active: true,
is_deleted: false,
created_at: None,

View File

@@ -377,7 +377,10 @@ async fn embeddings_route_rejects_chat_only_model() {
Some(EXECUTION_PATH_LOCAL_AUTH_DENIED)
);
let payload: serde_json::Value = response.json().await.expect("body should parse");
assert_eq!(payload["error"]["message"], "当前密钥不允许访问模型 gpt-5");
assert_eq!(
payload["error"]["message"],
"当前用户、用户组或密钥的访问控制策略不允许访问模型 gpt-5"
);
gateway_handle.abort();
}
@@ -416,7 +419,7 @@ async fn embeddings_route_rejects_chat_only_api_format() {
let payload: serde_json::Value = response.json().await.expect("body should parse");
assert_eq!(
payload["error"]["message"],
"当前密钥不允许访问 openai:embedding 格式"
"当前用户、用户组或密钥的访问控制策略不允许访问 openai:embedding 格式"
);
gateway_handle.abort();

View File

@@ -501,7 +501,7 @@ async fn gateway_locally_denies_disallowed_claude_api_format_without_hitting_con
assert_eq!(payload["error"]["type"], "http_error");
assert_eq!(
payload["error"]["message"],
"当前密钥不允许访问 claude:messages 格式"
"当前用户、用户组或密钥的访问控制策略不允许访问 claude:messages 格式"
);
assert_eq!(*auth_context_hits.lock().expect("mutex should lock"), 0);
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
@@ -584,7 +584,7 @@ async fn gateway_locally_denies_disallowed_provider_without_hitting_control_or_u
assert_eq!(payload["error"]["type"], "http_error");
assert_eq!(
payload["error"]["message"],
"当前密钥不允许访问 claude 提供商"
"当前用户、用户组或密钥的访问控制策略不允许访问 claude 提供商"
);
assert_eq!(*auth_context_hits.lock().expect("mutex should lock"), 0);
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
@@ -656,7 +656,7 @@ async fn gateway_locally_denies_disallowed_gemini_model_without_hitting_control_
assert_eq!(payload["error"]["type"], "http_error");
assert_eq!(
payload["error"]["message"],
"当前密钥不允许访问模型 gemini-2.5-pro"
"当前用户、用户组或密钥的访问控制策略不允许访问模型 gemini-2.5-pro"
);
assert_eq!(*auth_context_hits.lock().expect("mutex should lock"), 0);
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
@@ -806,7 +806,10 @@ async fn gateway_locally_denies_disallowed_openai_model_without_hitting_control_
);
let payload: serde_json::Value = response.json().await.expect("response json should parse");
assert_eq!(payload["error"]["type"], "http_error");
assert_eq!(payload["error"]["message"], "当前密钥不允许访问模型 gpt-5");
assert_eq!(
payload["error"]["message"],
"当前用户、用户组或密钥的访问控制策略不允许访问模型 gpt-5"
);
assert_eq!(*auth_context_hits.lock().expect("mutex should lock"), 0);
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);

View File

@@ -319,7 +319,7 @@ async fn rerank_route_rejects_chat_only_api_format() {
let payload: serde_json::Value = response.json().await.expect("body should parse");
assert_eq!(
payload["error"]["message"],
"当前密钥不允许访问 openai:rerank 格式"
"当前用户、用户组或密钥的访问控制策略不允许访问 openai:rerank 格式"
);
gateway_handle.abort();