mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b11e1ecf4b | ||
|
|
a34d3ca7bc |
@@ -586,11 +586,22 @@ pub fn normalized_imported_account_status(
|
||||
status: AccountStatus,
|
||||
source: AccountImportSource,
|
||||
access_token: &str,
|
||||
access_token_expires_at: Option<&DateTime<Utc>>,
|
||||
) -> AccountStatus {
|
||||
if source == AccountImportSource::Sub2Api
|
||||
&& status == AccountStatus::Active
|
||||
&& jwt_expiry(access_token, Utc::now()) != JwtExpiry::Valid
|
||||
{
|
||||
if source != AccountImportSource::Sub2Api || status != AccountStatus::Active {
|
||||
return status;
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
if let Some(expires_at) = access_token_expires_at {
|
||||
return if now >= *expires_at {
|
||||
AccountStatus::Expired
|
||||
} else {
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
if jwt_expiry(access_token, now) != JwtExpiry::Valid {
|
||||
AccountStatus::Expired
|
||||
} else {
|
||||
status
|
||||
|
||||
@@ -83,16 +83,17 @@ impl AccountManageService {
|
||||
let parsed_status =
|
||||
crate::fleet::import::parse_account_import_status(entry.status.as_deref())
|
||||
.map_err(|error| AccountManageError::InvalidStatus(error.to_string()))?;
|
||||
let mut status = crate::fleet::import::normalized_imported_account_status(
|
||||
parsed_status,
|
||||
source,
|
||||
&resolved_tokens.access_token,
|
||||
);
|
||||
let access_token_expires_at = resolved_tokens
|
||||
.claims
|
||||
.as_ref()
|
||||
.map(|claims| claims.expires_at)
|
||||
.or(access_token_expires_at);
|
||||
let mut status = crate::fleet::import::normalized_imported_account_status(
|
||||
parsed_status,
|
||||
source,
|
||||
&resolved_tokens.access_token,
|
||||
access_token_expires_at.as_ref(),
|
||||
);
|
||||
let claims = resolved_tokens.claims.as_ref();
|
||||
let mut plan_type = claims
|
||||
.and_then(|claims| claims.plan_type.clone())
|
||||
|
||||
@@ -237,6 +237,89 @@ async fn admin_accounts_import_should_read_sub2api_backup_access_token_only_acco
|
||||
assert!(stored.next_refresh_at.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_accounts_import_should_keep_opaque_sub2api_token_active() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/wham/usage"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"account_id": "sub2api-opaque-account",
|
||||
"user_id": "sub2api-opaque-user",
|
||||
"email": "sub2api-opaque@example.com",
|
||||
"plan_type": "team",
|
||||
"rate_limit": {
|
||||
"allowed": true,
|
||||
"limit_reached": false,
|
||||
"primary_window": {
|
||||
"used_percent": 0,
|
||||
"reset_at": 1_800_000_000,
|
||||
"limit_window_seconds": 18_000
|
||||
}
|
||||
}
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let (app, _state, pool, _dir) = admin_accounts_test_app_with_api_base_url(
|
||||
"admin-accounts-import-sub2api-opaque-at",
|
||||
128,
|
||||
format!("{}/backend-api", server.uri()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/admin/accounts/import")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", "cpr_admin_session=session_1")
|
||||
.header("x-request-id", "req_accounts_import_sub2api_opaque_at")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"sourceFormat": "sub2api",
|
||||
"exported_at": "2026-07-19T18:05:29Z",
|
||||
"proxies": [],
|
||||
"accounts": [{
|
||||
"name": "sub2api-opaque@example.com",
|
||||
"platform": "openai",
|
||||
"type": "oauth",
|
||||
"credentials": {
|
||||
"access_token": "at-sub2api-opaque-token",
|
||||
"chatgpt_account_id": "sub2api-opaque-account",
|
||||
"chatgpt_user_id": "sub2api-opaque-user",
|
||||
"email": "sub2api-opaque@example.com",
|
||||
"plan_type": "team",
|
||||
"refresh_token": "",
|
||||
"expires_at": "2099-12-01T00:00:00Z"
|
||||
},
|
||||
"auto_pause_on_expired": true,
|
||||
"concurrency": 30,
|
||||
"priority": 1,
|
||||
"type": "oauth"
|
||||
}]
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response_json(response).await;
|
||||
let store = PgAccountStore::new(pool);
|
||||
let stored = store
|
||||
.find_by_chatgpt_identity("sub2api-opaque-account", Some("sub2api-opaque-user"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["data"]["imported"], 1);
|
||||
assert_eq!(stored.status, AccountStatus::Active);
|
||||
assert!(stored.access_token_expires_at.is_some());
|
||||
assert!(stored.refresh_token.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_accounts_import_should_read_cliproxyapi_codex_auth_files() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
@@ -212,6 +212,7 @@ const WEBSOCKET_SECOND_ACCOUNT_LIMITED: &str =
|
||||
mod responses_http;
|
||||
mod responses_recovery;
|
||||
mod responses_websocket;
|
||||
mod sub2api_import;
|
||||
mod usage_logging;
|
||||
|
||||
async fn test_app_state_with_pool(config: &AppConfig, pool: PgPool) -> AppState {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sub2api_opaque_access_token_with_future_expiry_should_handle_normal_responses_request() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/wham/usage"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"account_id": "sub2api-opaque-account",
|
||||
"user_id": "sub2api-opaque-user",
|
||||
"email": "sub2api-opaque@example.com",
|
||||
"plan_type": "team",
|
||||
"rate_limit": {
|
||||
"allowed": true,
|
||||
"limit_reached": false,
|
||||
"primary_window": {
|
||||
"used_percent": 0,
|
||||
"reset_at": 1_800_000_000,
|
||||
"limit_window_seconds": 18_000
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/codex/responses"))
|
||||
.and(header("authorization", "Bearer at-sub2api-opaque-token"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.insert_header("content-type", "text/event-stream")
|
||||
.set_body_string(RESPONSES_SUCCESS_SSE),
|
||||
)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let (pool, _dir) = init_test_db("sub2api-opaque-normal-responses").await;
|
||||
let api_key = insert_client_api_key(&pool).await;
|
||||
let state = test_app_state_with_pool(
|
||||
&test_config(test_database_url(), format!("{}/backend-api", server.uri())),
|
||||
pool,
|
||||
)
|
||||
.await;
|
||||
let imported = state
|
||||
.services
|
||||
.admin_accounts
|
||||
.import(json!({
|
||||
"sourceFormat": "sub2api",
|
||||
"exported_at": "2026-07-19T18:05:29Z",
|
||||
"proxies": [],
|
||||
"accounts": [{
|
||||
"name": "sub2api-opaque@example.com",
|
||||
"platform": "openai",
|
||||
"type": "oauth",
|
||||
"credentials": {
|
||||
"access_token": "at-sub2api-opaque-token",
|
||||
"chatgpt_account_id": "sub2api-opaque-account",
|
||||
"chatgpt_user_id": "sub2api-opaque-user",
|
||||
"email": "sub2api-opaque@example.com",
|
||||
"plan_type": "team",
|
||||
"refresh_token": "",
|
||||
"expires_at": "2099-12-01T00:00:00Z"
|
||||
},
|
||||
"auto_pause_on_expired": true,
|
||||
"concurrency": 30,
|
||||
"priority": 1,
|
||||
"type": "oauth"
|
||||
}]
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(imported.imported, 1);
|
||||
|
||||
let app = router::router().with_state(state);
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/responses")
|
||||
.header("authorization", format!("Bearer {api_key}"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"model": "gpt-5.5",
|
||||
"input": [],
|
||||
"stream": false,
|
||||
"use_websocket": false
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response_json(response).await;
|
||||
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["id"], "resp_response_1");
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
version: "2.5.2"
|
||||
version: "2.5.3"
|
||||
|
||||
Reference in New Issue
Block a user