refactor: store tokens and api keys plainly

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-06-27 21:55:47 +08:00
co-authored by Codex
parent 71d192db45
commit 159eb49e3d
40 changed files with 285 additions and 437 deletions
+7 -23
View File
@@ -4,13 +4,11 @@ use std::sync::Arc;
use async_trait::async_trait;
use chrono::Utc;
use secrecy::{ExposeSecret, SecretString};
use sqlx::{Row, SqlitePool};
use thiserror::Error;
use uuid::Uuid;
use crate::infra::{
crypto::{CryptoError, SecretBox},
identity::{ApiKeyHasher, AuthError},
json::{decode_cursor, encode_cursor, Page},
};
@@ -332,9 +330,6 @@ pub enum SqliteClientKeyStoreError {
/// API Key 验证错误。
#[error("sqlite client key auth error: {0}")]
Auth(#[from] AuthError),
/// API Key 密文处理错误。
#[error("sqlite client key crypto error: {0}")]
Crypto(#[from] CryptoError),
/// 分页游标无效。
#[error("invalid client key pagination cursor")]
InvalidCursor,
@@ -387,18 +382,12 @@ pub struct CreatedClientApiKey {
pub struct SqliteClientKeyStore {
pool: SqlitePool,
hasher: ApiKeyHasher,
secret_box: SecretBox,
}
impl SqliteClientKeyStore {
/// 构造存储适配器。
pub fn new(pool: SqlitePool, hasher: ApiKeyHasher) -> Self {
let secret_box = hasher.secret_box();
Self {
pool,
hasher,
secret_box,
}
Self { pool, hasher }
}
/// 暴露底层连接池,供集成测试和运行时组合层复用。
@@ -414,17 +403,14 @@ impl SqliteClientKeyStore {
let generated = self.hasher.generate_client_api_key(name);
let id = format!("key_{}", Uuid::new_v4().simple());
let now = Utc::now().to_rfc3339();
let key_cipher = self
.secret_box
.encrypt(&SecretString::new(generated.key.clone().into()))?;
sqlx::query(
"insert into client_api_keys (id, name, label, prefix, key_hash, key_cipher, enabled, created_at, last_used_at) values (?, ?, null, ?, ?, ?, 1, ?, null)",
"insert into client_api_keys (id, name, label, prefix, key_hash, key, enabled, created_at, last_used_at) values (?, ?, null, ?, ?, ?, 1, ?, null)",
)
.bind(&id)
.bind(name)
.bind(&generated.prefix)
.bind(&generated.key_hash)
.bind(&key_cipher)
.bind(&generated.key)
.bind(&now)
.execute(&self.pool)
.await?;
@@ -452,7 +438,7 @@ impl SqliteClientKeyStore {
let (created_at, id) =
decode_cursor(&cursor).ok_or(SqliteClientKeyStoreError::InvalidCursor)?;
sqlx::query(
"select id, name, label, prefix, key_cipher, enabled, created_at, last_used_at from client_api_keys where created_at < ? or (created_at = ? and id < ?) order by created_at desc, id desc limit ?",
"select id, name, label, prefix, key, enabled, created_at, last_used_at from client_api_keys where created_at < ? or (created_at = ? and id < ?) order by created_at desc, id desc limit ?",
)
.bind(&created_at)
.bind(created_at)
@@ -462,7 +448,7 @@ impl SqliteClientKeyStore {
.await?
} else {
sqlx::query(
"select id, name, label, prefix, key_cipher, enabled, created_at, last_used_at from client_api_keys order by created_at desc, id desc limit ?",
"select id, name, label, prefix, key, enabled, created_at, last_used_at from client_api_keys order by created_at desc, id desc limit ?",
)
.bind(fetch_limit)
.fetch_all(&self.pool)
@@ -493,7 +479,7 @@ impl SqliteClientKeyStore {
id: &str,
) -> Result<Option<StoredClientApiKey>, SqliteClientKeyStoreError> {
let row = sqlx::query(
"select id, name, label, prefix, key_cipher, enabled, created_at, last_used_at from client_api_keys where id = ?",
"select id, name, label, prefix, key, enabled, created_at, last_used_at from client_api_keys where id = ?",
)
.bind(id)
.fetch_optional(&self.pool)
@@ -549,14 +535,12 @@ impl SqliteClientKeyStore {
&self,
row: &sqlx::sqlite::SqliteRow,
) -> Result<StoredClientApiKey, SqliteClientKeyStoreError> {
let key_cipher = row.get::<String, _>("key_cipher");
let key = self.secret_box.decrypt(&key_cipher)?;
Ok(StoredClientApiKey {
id: row.get("id"),
name: row.get("name"),
label: row.get("label"),
prefix: row.get("prefix"),
key: key.expose_secret().to_string(),
key: row.get("key"),
enabled: row.get::<i64, _>("enabled") != 0,
created_at: row.get("created_at"),
last_used_at: row.get("last_used_at"),
-6
View File
@@ -12,8 +12,6 @@ use rand::Rng;
use sha2::Sha256;
use thiserror::Error;
use crate::infra::crypto::SecretBox;
// ---------------------------------------------------------------------------
// AuthError / AuthResult
// ---------------------------------------------------------------------------
@@ -164,8 +162,4 @@ impl ApiKeyHasher {
Err(error) => unreachable!("HMAC accepts any key size: {error}"),
}
}
pub(crate) fn secret_box(&self) -> SecretBox {
SecretBox::new(self.pepper)
}
}
+3 -3
View File
@@ -39,7 +39,7 @@ create table if not exists client_api_keys (
name text not null,
prefix text not null,
key_hash text not null,
key_cipher text not null,
key text not null,
label text,
enabled integer not null default 1 check (enabled in (0, 1)),
created_at text not null,
@@ -58,8 +58,8 @@ create table if not exists accounts (
chatgpt_user_id text,
label text,
plan_type text,
access_token_cipher text not null,
refresh_token_cipher text,
access_token text not null,
refresh_token text,
access_token_expires_at text,
next_refresh_at text,
status text not null check (status in ('active', 'expired', 'quota_exhausted', 'refreshing', 'disabled', 'banned')),
+1 -4
View File
@@ -41,10 +41,7 @@ async fn build_application(
let fingerprint_repository = FingerprintRepository::new(pool.clone());
let stores = BackgroundTaskStores {
accounts: crate::upstream::accounts::store::SqliteAccountStore::new(
pool.clone(),
secret.clone(),
),
accounts: crate::upstream::accounts::store::SqliteAccountStore::new(pool.clone()),
admin_sessions: crate::admin::auth::service::SqliteAdminSessionStore::new(pool.clone()),
cookies: crate::upstream::accounts::cookies::SqliteCookieStore::new(
pool.clone(),
+57 -89
View File
@@ -7,7 +7,6 @@ use serde_json::Value;
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite, SqlitePool};
use thiserror::Error;
use crate::infra::crypto::{CryptoError, SecretBox};
use crate::infra::json::{decode_cursor, page_offset, NumberedPage, Page};
use crate::upstream::accounts::model::{Account, AccountStatus, AccountUsageDelta};
@@ -23,8 +22,8 @@ select
a.chatgpt_user_id as user_id,
a.label,
a.plan_type,
a.access_token_cipher,
a.refresh_token_cipher,
a.access_token,
a.refresh_token,
a.access_token_expires_at,
a.next_refresh_at,
a.status,
@@ -63,8 +62,8 @@ select
a.chatgpt_user_id as user_id,
a.label,
a.plan_type,
a.access_token_cipher,
a.refresh_token_cipher,
a.access_token,
a.refresh_token,
a.access_token_expires_at,
a.next_refresh_at,
a.status,
@@ -103,8 +102,8 @@ insert into accounts (
chatgpt_user_id,
label,
plan_type,
access_token_cipher,
refresh_token_cipher,
access_token,
refresh_token,
access_token_expires_at,
status,
added_at,
@@ -119,8 +118,8 @@ select
chatgpt_user_id as user_id,
label,
plan_type,
access_token_cipher,
refresh_token_cipher,
access_token,
refresh_token,
access_token_expires_at,
next_refresh_at,
status,
@@ -137,8 +136,8 @@ select
chatgpt_user_id as user_id,
label,
plan_type,
access_token_cipher,
refresh_token_cipher,
access_token,
refresh_token,
access_token_expires_at,
next_refresh_at,
status,
@@ -185,8 +184,8 @@ select
chatgpt_user_id as user_id,
label,
plan_type,
access_token_cipher,
refresh_token_cipher,
access_token,
refresh_token,
access_token_expires_at,
next_refresh_at,
status,
@@ -206,8 +205,8 @@ select
chatgpt_user_id as user_id,
label,
plan_type,
access_token_cipher,
refresh_token_cipher,
access_token,
refresh_token,
access_token_expires_at,
next_refresh_at,
status,
@@ -472,8 +471,8 @@ set
chatgpt_account_id = ?,
chatgpt_user_id = ?,
plan_type = ?,
access_token_cipher = ?,
refresh_token_cipher = ?,
access_token = ?,
refresh_token = ?,
access_token_expires_at = ?,
next_refresh_at = ?,
status = ?,
@@ -487,7 +486,7 @@ set
chatgpt_account_id = ?,
chatgpt_user_id = ?,
plan_type = ?,
access_token_cipher = ?,
access_token = ?,
access_token_expires_at = ?,
next_refresh_at = ?,
status = ?,
@@ -509,8 +508,8 @@ set
chatgpt_user_id = ?,
label = ?,
plan_type = ?,
access_token_cipher = ?,
refresh_token_cipher = ?,
access_token = ?,
refresh_token = ?,
access_token_expires_at = ?,
status = ?,
quota_json = coalesce(?, quota_json),
@@ -529,7 +528,7 @@ set
chatgpt_user_id = ?,
label = ?,
plan_type = ?,
access_token_cipher = ?,
access_token = ?,
access_token_expires_at = ?,
status = ?,
quota_json = coalesce(?, quota_json),
@@ -566,9 +565,6 @@ pub enum SqliteAccountStoreError {
/// 数据库错误。
#[error("sqlite account store database error: {0}")]
Database(#[from] sqlx::Error),
/// 加解密错误。
#[error("sqlite account store crypto error: {0}")]
Crypto(#[from] CryptoError),
/// 时间格式错误。
#[error("sqlite account store timestamp error: {0}")]
Timestamp(#[from] chrono::ParseError),
@@ -665,9 +661,9 @@ pub struct StoredAccount {
pub label: Option<String>,
/// 计划类型。
pub plan_type: Option<String>,
/// access token 密文。
/// access token 明文。
pub access_token: SecretString,
/// refresh token 密文。
/// refresh token 明文。
pub refresh_token: Option<SecretString>,
/// access token 过期时间。
pub access_token_expires_at: Option<DateTime<Utc>>,
@@ -917,13 +913,12 @@ pub trait AccountStore: Send + Sync + 'static {
#[derive(Clone)]
pub struct SqliteAccountStore {
pool: SqlitePool,
secret_box: SecretBox,
}
impl SqliteAccountStore {
/// 构造存储。
pub fn new(pool: SqlitePool, secret_box: SecretBox) -> Self {
Self { pool, secret_box }
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
/// 返回底层连接池。
@@ -934,11 +929,10 @@ impl SqliteAccountStore {
/// 插入新账号。
pub async fn insert(&self, account: NewAccount) -> SqliteAccountStoreResult<()> {
let now = Utc::now().to_rfc3339();
let access_token_cipher = self.secret_box.encrypt(&account.access_token)?;
let refresh_token_cipher = account
let refresh_token = account
.refresh_token
.map(|token| self.secret_box.encrypt(&token))
.transpose()?;
.as_ref()
.map(ExposeSecret::expose_secret);
sqlx::query(INSERT_ACCOUNT_SQL)
.bind(&account.id)
.bind(&account.email)
@@ -946,8 +940,8 @@ impl SqliteAccountStore {
.bind(&account.user_id)
.bind(&account.label)
.bind(&account.plan_type)
.bind(&access_token_cipher)
.bind(&refresh_token_cipher)
.bind(account.access_token.expose_secret())
.bind(refresh_token)
.bind(account.access_token_expires_at.map(|dt| dt.to_rfc3339()))
.bind(status_to_db(account.status))
.bind(
@@ -968,9 +962,7 @@ impl SqliteAccountStore {
.bind(account_id)
.fetch_optional(&self.pool)
.await?;
row.as_ref()
.map(|row| stored_account_from_row(self, row))
.transpose()
row.as_ref().map(stored_account_from_row).transpose()
}
/// 通过 ChatGPT 身份查找账号。
@@ -985,9 +977,7 @@ impl SqliteAccountStore {
.bind(chatgpt_user_id)
.fetch_optional(&self.pool)
.await?;
row.as_ref()
.map(|row| stored_account_from_row(self, row))
.transpose()
row.as_ref().map(stored_account_from_row).transpose()
}
/// 分页列出所有账号(含 token)。
@@ -1010,7 +1000,7 @@ impl SqliteAccountStore {
Ok(to_page(
rows,
limit,
|row| stored_account_from_row(self, row),
stored_account_from_row,
("added_at", "id"),
))
} else {
@@ -1021,7 +1011,7 @@ impl SqliteAccountStore {
Ok(to_page(
rows,
limit,
|row| stored_account_from_row(self, row),
stored_account_from_row,
("added_at", "id"),
))
}
@@ -1137,20 +1127,19 @@ impl SqliteAccountStore {
update: AccountClaimsUpdate,
) -> SqliteAccountStoreResult<bool> {
let now = Utc::now().to_rfc3339();
let access_token_cipher = self.secret_box.encrypt(&update.access_token)?;
let refresh_token_cipher = update
let refresh_token = update
.refresh_token
.map(|token| self.secret_box.encrypt(&token))
.transpose()?;
.as_ref()
.map(ExposeSecret::expose_secret);
let result = if let Some(refresh_cipher) = &refresh_token_cipher {
let result = if let Some(refresh_token) = refresh_token {
sqlx::query(UPDATE_ACCOUNT_CLAIMS_WITH_REFRESH_SQL)
.bind(&update.email)
.bind(&update.account_id)
.bind(&update.user_id)
.bind(&update.plan_type)
.bind(&access_token_cipher)
.bind(refresh_cipher)
.bind(update.access_token.expose_secret())
.bind(refresh_token)
.bind(update.access_token_expires_at.map(|dt| dt.to_rfc3339()))
.bind(update.next_refresh_at.map(|dt| dt.to_rfc3339()))
.bind(status_to_db(update.status))
@@ -1164,7 +1153,7 @@ impl SqliteAccountStore {
.bind(&update.account_id)
.bind(&update.user_id)
.bind(&update.plan_type)
.bind(&access_token_cipher)
.bind(update.access_token.expose_secret())
.bind(update.access_token_expires_at.map(|dt| dt.to_rfc3339()))
.bind(update.next_refresh_at.map(|dt| dt.to_rfc3339()))
.bind(status_to_db(update.status))
@@ -1182,25 +1171,24 @@ impl SqliteAccountStore {
update: ImportedAccountUpdate,
) -> SqliteAccountStoreResult<bool> {
let now = Utc::now().to_rfc3339();
let access_token_cipher = self.secret_box.encrypt(&update.account.access_token)?;
let refresh_token_cipher = update
let refresh_token = update
.account
.refresh_token
.map(|token| self.secret_box.encrypt(&token))
.transpose()?;
.as_ref()
.map(ExposeSecret::expose_secret);
let quota_json = update.quota_json;
let quota_fetched_at = update.quota_fetched_at.map(|dt| dt.to_rfc3339());
let quota_verify_required = if update.quota_verify_required { 1 } else { 0 };
let result = if let Some(refresh_cipher) = &refresh_token_cipher {
let result = if let Some(refresh_token) = refresh_token {
sqlx::query(UPDATE_IMPORTED_ACCOUNT_WITH_REFRESH_SQL)
.bind(&update.account.email)
.bind(&update.account.account_id)
.bind(&update.account.user_id)
.bind(&update.account.label)
.bind(&update.account.plan_type)
.bind(&access_token_cipher)
.bind(refresh_cipher)
.bind(update.account.access_token.expose_secret())
.bind(refresh_token)
.bind(
update
.account
@@ -1223,7 +1211,7 @@ impl SqliteAccountStore {
.bind(&update.account.user_id)
.bind(&update.account.label)
.bind(&update.account.plan_type)
.bind(&access_token_cipher)
.bind(update.account.access_token.expose_secret())
.bind(
update
.account
@@ -1660,7 +1648,7 @@ async fn list_pool_accounts(store: &SqliteAccountStore) -> SqliteAccountStoreRes
let mut accounts = Vec::with_capacity(rows.len());
for row in rows {
accounts.push(pool_account_from_row(store, &row)?);
accounts.push(pool_account_from_row(&row)?);
}
Ok(accounts)
@@ -1674,22 +1662,10 @@ async fn get_pool_account(
.bind(account_id)
.fetch_optional(&store.pool)
.await?;
row.map(|row| pool_account_from_row(store, &row))
.transpose()
row.map(|row| pool_account_from_row(&row)).transpose()
}
fn pool_account_from_row(
store: &SqliteAccountStore,
row: &SqliteRow,
) -> SqliteAccountStoreResult<Account> {
let access_token_cipher = row.get::<String, _>("access_token_cipher");
let access_token = store.secret_box.decrypt(&access_token_cipher)?;
let refresh_token = row
.get::<Option<String>, _>("refresh_token_cipher")
.map(|cipher| store.secret_box.decrypt(&cipher))
.transpose()?
.map(|token| token.expose_secret().to_string());
fn pool_account_from_row(row: &SqliteRow) -> SqliteAccountStoreResult<Account> {
Ok(Account {
id: row.get("id"),
email: row.get("email"),
@@ -1697,8 +1673,8 @@ fn pool_account_from_row(
user_id: row.get("user_id"),
label: row.get("label"),
plan_type: row.get("plan_type"),
access_token: access_token.expose_secret().to_string(),
refresh_token,
access_token: row.get("access_token"),
refresh_token: row.get("refresh_token"),
access_token_expires_at: parse_optional_rfc3339(
row.get::<Option<String>, _>("access_token_expires_at"),
)?,
@@ -1758,17 +1734,7 @@ fn pool_account_from_row(
})
}
fn stored_account_from_row(
store: &SqliteAccountStore,
row: &SqliteRow,
) -> SqliteAccountStoreResult<StoredAccount> {
let access_token_cipher = row.get::<String, _>("access_token_cipher");
let access_token = store.secret_box.decrypt(&access_token_cipher)?;
let refresh_token = row
.get::<Option<String>, _>("refresh_token_cipher")
.map(|cipher| store.secret_box.decrypt(&cipher))
.transpose()?;
fn stored_account_from_row(row: &SqliteRow) -> SqliteAccountStoreResult<StoredAccount> {
Ok(StoredAccount {
id: row.get("id"),
email: row.get("email"),
@@ -1776,8 +1742,10 @@ fn stored_account_from_row(
user_id: row.get("user_id"),
label: row.get("label"),
plan_type: row.get("plan_type"),
access_token,
refresh_token,
access_token: SecretString::new(row.get::<String, _>("access_token").into()),
refresh_token: row
.get::<Option<String>, _>("refresh_token")
.map(|token| SecretString::new(token.into())),
access_token_expires_at: parse_optional_rfc3339(
row.get::<Option<String>, _>("access_token_expires_at"),
)?,
+25 -31
View File
@@ -12,9 +12,8 @@ async fn admin_accounts_export_should_return_native_account_tokens() {
let pool = connect_sqlite(&url).await.unwrap();
seed_admin_session(&pool, "session_1").await;
let secret_box = SecretBox::new([77u8; 32]);
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_export".to_string(),
email: Some("export@example.com".to_string()),
@@ -33,7 +32,7 @@ async fn admin_accounts_export_should_return_native_account_tokens() {
let config = test_config(url);
let hasher = ApiKeyHasher::new([78u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box.clone()),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -91,7 +90,7 @@ async fn admin_accounts_import_should_store_native_account_tokens() {
let config = test_config(url);
let hasher = ApiKeyHasher::new([80u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box.clone()),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -134,7 +133,7 @@ async fn admin_accounts_import_should_store_native_account_tokens() {
.unwrap();
let status = response.status();
let body = response_json(response).await;
let stored = SqliteAccountStore::new(pool, secret_box)
let stored = SqliteAccountStore::new(pool)
.get("acct_import")
.await
.unwrap()
@@ -191,7 +190,7 @@ async fn admin_accounts_import_should_fetch_wham_usage_for_current_openai_token_
})))
.mount(&server)
.await;
let (app, _state, pool, _dir, secret_box) = admin_accounts_test_app_with_api_base_url(
let (app, _state, pool, _dir, _) = admin_accounts_test_app_with_api_base_url(
"admin-accounts-import-current-openai-token.sqlite",
118,
format!("{}/backend-api", server.uri()),
@@ -240,7 +239,7 @@ async fn admin_accounts_import_should_fetch_wham_usage_for_current_openai_token_
.unwrap();
let status = response.status();
let body = response_json(response).await;
let stored = SqliteAccountStore::new(pool, secret_box)
let stored = SqliteAccountStore::new(pool)
.get("a18bcfa9ae932857")
.await
.unwrap()
@@ -296,7 +295,7 @@ async fn admin_accounts_import_should_complete_chatgpt_account_id_from_refresh_t
})))
.mount(&server)
.await;
let (app, _state, pool, _dir, secret_box) =
let (app, _state, pool, _dir, _) =
admin_accounts_test_app_with_api_base_url_and_oauth_token_endpoint(
"admin-accounts-import-rt-complete.sqlite",
125,
@@ -325,7 +324,7 @@ async fn admin_accounts_import_should_complete_chatgpt_account_id_from_refresh_t
.await
.unwrap();
let body = response_json(response).await;
let stored = SqliteAccountStore::new(pool, secret_box)
let stored = SqliteAccountStore::new(pool)
.list_metadata_page(1, 10, None)
.await
.unwrap()
@@ -388,7 +387,7 @@ async fn admin_accounts_import_should_fetch_usage_to_complete_missing_plan_and_q
config.api.base_url = format!("{}/backend-api", server.uri());
let hasher = ApiKeyHasher::new([121u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box.clone()),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -441,7 +440,7 @@ async fn admin_accounts_import_should_fetch_usage_to_complete_missing_plan_and_q
.unwrap();
let status = response.status();
let body = response_json(response).await;
let stored = SqliteAccountStore::new(pool.clone(), secret_box)
let stored = SqliteAccountStore::new(pool.clone())
.get("acct_import_supplement")
.await
.unwrap()
@@ -500,7 +499,7 @@ async fn admin_accounts_import_should_fetch_usage_to_complete_missing_plan_and_q
#[tokio::test]
async fn admin_accounts_import_should_update_existing_sub2api_account_and_clear_stale_quota_lock() {
let (app, _state, pool, _dir, secret_box) =
let (app, _state, pool, _dir, _) =
admin_accounts_test_app("admin-accounts-import-update-existing.sqlite", 116).await;
let new_access_token = test_jwt(
"chatgpt-import-update",
@@ -508,9 +507,8 @@ async fn admin_accounts_import_should_update_existing_sub2api_account_and_clear_
Some("new@example.com"),
Some("plus"),
);
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_import_update".to_string(),
email: Some("old@example.com".to_string()),
@@ -580,7 +578,7 @@ async fn admin_accounts_import_should_update_existing_sub2api_account_and_clear_
.unwrap();
let status = response.status();
let body = response_json(response).await;
let stored = SqliteAccountStore::new(pool.clone(), secret_box)
let stored = SqliteAccountStore::new(pool.clone())
.get("acct_import_update")
.await
.unwrap()
@@ -608,7 +606,7 @@ async fn admin_accounts_import_should_require_admin_session_cookie() {
let secret_box = SecretBox::new([111u8; 32]);
let hasher = ApiKeyHasher::new([112u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box.clone()),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -642,7 +640,7 @@ async fn admin_accounts_import_should_require_admin_session_cookie() {
}
#[tokio::test]
async fn admin_accounts_import_should_store_tokens_encrypted_and_list_sanitized_accounts() {
async fn admin_accounts_import_should_store_plain_tokens_and_list_sanitized_accounts() {
let (app, _state, pool, _dir, _secret_box) =
admin_accounts_test_app("admin-accounts-import-sanitized.sqlite", 113).await;
let access_token = test_jwt(
@@ -670,17 +668,14 @@ async fn admin_accounts_import_should_store_tokens_encrypted_and_list_sanitized_
let body = response_json(response).await;
assert_eq!(body["data"]["imported"], 1);
let stored: (String, String) = sqlx::query_as(
"select access_token_cipher, refresh_token_cipher from accounts where id = ?",
)
.bind("acct_imported_sanitized")
.fetch_one(&pool)
.await
.unwrap();
assert!(stored.0.starts_with("v1:"));
assert!(!stored.0.contains(&access_token));
assert!(stored.1.starts_with("v1:"));
assert!(!stored.1.contains("refresh-secret"));
let stored: (String, String) =
sqlx::query_as("select access_token, refresh_token from accounts where id = ?")
.bind("acct_imported_sanitized")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(stored.0, access_token);
assert_eq!(stored.1, "refresh-secret");
let list = app
.clone()
@@ -705,11 +700,10 @@ async fn admin_accounts_import_should_store_tokens_encrypted_and_list_sanitized_
#[tokio::test]
async fn admin_accounts_export_should_return_native_accounts_with_tokens_and_filter_ids() {
let (app, _state, pool, _dir, secret_box) =
let (app, _state, pool, _dir, _) =
admin_accounts_test_app("admin-accounts-export-filter.sqlite", 117).await;
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_export_a".to_string(),
email: Some("export-a@example.com".to_string()),
+19 -22
View File
@@ -7,16 +7,16 @@ async fn admin_accounts_lifecycle_should_update_and_delete_accounts() {
let url = format!("sqlite://{}", db.display());
let pool = connect_sqlite(&url).await.unwrap();
seed_admin_session(&pool, "session_1").await;
sqlx::query("insert into accounts (id, email, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
sqlx::query("insert into accounts (id, email, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
.bind("acct_lifecycle").bind("life@example.com").bind("cipher").bind("active")
.bind("2026-06-18T00:00:00Z").bind("2026-06-18T00:00:00Z").execute(&pool).await.unwrap();
let config = test_config(url);
let secret_box = SecretBox::new([75u8; 32]);
let hasher = ApiKeyHasher::new([76u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([75u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -118,11 +118,10 @@ async fn admin_accounts_lifecycle_should_update_and_delete_accounts() {
#[tokio::test]
async fn admin_account_status_update_should_update_proxy_account_pool() {
let (app, state, pool, _dir, secret_box) =
let (app, state, pool, _dir, _) =
admin_accounts_test_app("admin-account-status-runtime-pool.sqlite", 121).await;
seed_encrypted_account(
seed_account(
&pool,
secret_box,
NewAccount {
id: "acct_runtime_status".to_string(),
email: Some("runtime-status@example.com".to_string()),
@@ -200,15 +199,14 @@ async fn admin_account_refresh_should_not_mark_valid_account_banned_when_refresh
})))
.mount(&server)
.await;
let (app, _state, pool, _dir, secret_box) = admin_accounts_test_app_with_oauth_token_endpoint(
let (app, _state, pool, _dir, _) = admin_accounts_test_app_with_oauth_token_endpoint(
"admin-account-refresh-rt-reused.sqlite",
124,
format!("{}/oauth/token", server.uri()),
)
.await;
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_refresh_rt_reused".to_string(),
email: Some("rt-reused@example.com".to_string()),
@@ -247,7 +245,7 @@ async fn admin_account_refresh_should_not_mark_valid_account_banned_when_refresh
)
.await
.unwrap();
let stored = SqliteAccountStore::new(pool, secret_box)
let stored = SqliteAccountStore::new(pool)
.get("acct_refresh_rt_reused")
.await
.unwrap()
@@ -261,10 +259,10 @@ async fn admin_account_refresh_should_not_mark_valid_account_banned_when_refresh
async fn admin_account_update_should_accept_batch_status_payload() {
let (app, _state, pool, _dir, _secret_box) =
admin_accounts_test_app("admin-account-batch-status-update.sqlite", 122).await;
sqlx::query("insert into accounts (id, email, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
sqlx::query("insert into accounts (id, email, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
.bind("acct_batch_status_1").bind("batch-1@example.com").bind("cipher").bind("active")
.bind("2026-06-18T00:00:00Z").bind("2026-06-18T00:00:00Z").execute(&pool).await.unwrap();
sqlx::query("insert into accounts (id, email, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
sqlx::query("insert into accounts (id, email, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
.bind("acct_batch_status_2").bind("batch-2@example.com").bind("cipher").bind("active")
.bind("2026-06-18T00:00:00Z").bind("2026-06-18T00:00:00Z").execute(&pool).await.unwrap();
@@ -325,9 +323,8 @@ async fn admin_account_cookies_should_store_encrypted_cookie_header() {
let pool = connect_sqlite(&url).await.unwrap();
seed_admin_session(&pool, "session_1").await;
let secret_box = SecretBox::new([85u8; 32]);
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_cookies".to_string(),
email: Some("cookies@example.com".to_string()),
@@ -346,9 +343,9 @@ async fn admin_account_cookies_should_store_encrypted_cookie_header() {
let config = test_config(url);
let hasher = ApiKeyHasher::new([86u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([85u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -414,7 +411,7 @@ async fn admin_account_cookies_should_store_encrypted_cookie_header() {
}
#[tokio::test]
async fn admin_account_create_should_derive_claims_and_store_encrypted_tokens() {
async fn admin_account_create_should_derive_claims_and_store_plain_tokens() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("admin-account-create.sqlite");
let url = format!("sqlite://{}", db.display());
@@ -430,7 +427,7 @@ async fn admin_account_create_should_derive_claims_and_store_encrypted_tokens()
let config = test_config(url);
let hasher = ApiKeyHasher::new([92u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box.clone()),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -476,9 +473,9 @@ async fn admin_account_manual_create_should_reject_missing_invalid_expired_or_un
let secret_box = SecretBox::new([93u8; 32]);
let hasher = ApiKeyHasher::new([94u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([93u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -510,7 +507,7 @@ async fn admin_account_manual_create_should_reject_missing_invalid_expired_or_un
#[tokio::test]
async fn admin_account_manual_create_should_accept_current_openai_token_without_chatgpt_account_id()
{
let (app, _state, pool, _dir, secret_box) =
let (app, _state, pool, _dir, _) =
admin_accounts_test_app("admin-account-create-current-openai-token.sqlite", 119).await;
let token = test_jwt_with_exp(
None,
@@ -527,7 +524,7 @@ async fn admin_account_manual_create_should_accept_current_openai_token_without_
.await;
let status = response.status();
let body = response_json(response).await;
let stored = SqliteAccountStore::new(pool, secret_box)
let stored = SqliteAccountStore::new(pool)
.get(body["data"]["id"].as_str().unwrap())
.await
.unwrap()
+7 -7
View File
@@ -2,23 +2,23 @@ use super::*;
use codex_proxy_rs::admin::monitoring::events::{EventLevel, EventLog};
#[tokio::test]
async fn admin_accounts_list_should_not_decrypt_account_tokens() {
async fn admin_accounts_list_should_not_expose_account_tokens() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("admin-accounts.sqlite");
let url = format!("sqlite://{}", db.display());
let pool = connect_sqlite(&url).await.unwrap();
seed_admin_session(&pool, "session_1").await;
sqlx::query("insert into accounts (id, email, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
.bind("acct_corrupt").bind("user@example.com").bind("not-a-secret-box-cipher")
sqlx::query("insert into accounts (id, email, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)")
.bind("acct_plain").bind("user@example.com").bind("plain-access-token")
.bind("active").bind("2026-06-18T00:00:00Z").bind("2026-06-18T00:00:00Z")
.execute(&pool).await.unwrap();
let config = test_config(url);
let secret_box = SecretBox::new([63u8; 32]);
let hasher = ApiKeyHasher::new([64u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([63u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -48,7 +48,7 @@ async fn admin_accounts_list_should_not_decrypt_account_tokens() {
assert_eq!(response.status(), StatusCode::OK);
let body = response_json(response).await;
assert_eq!(body["data"]["items"][0]["id"], "acct_corrupt");
assert_eq!(body["data"]["items"][0]["id"], "acct_plain");
assert_eq!(body["data"]["items"][0]["email"], "user@example.com");
assert_eq!(
body["data"]["items"][0]["addedAt"],
@@ -221,7 +221,7 @@ async fn admin_accounts_list_should_return_numbered_page_with_search_total() {
"2026-06-18T00:00:00Z",
),
] {
sqlx::query("insert into accounts (id, email, label, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)")
sqlx::query("insert into accounts (id, email, label, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)")
.bind(id)
.bind(email)
.bind(label)
+5 -5
View File
@@ -78,8 +78,8 @@ async fn seed_usage_account(
cached_tokens: i64,
last_used_at: &str,
) {
sqlx::query("insert into accounts (id, email, label, plan_type, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, 'active', ?, ?)")
.bind(id).bind(email).bind(label).bind(plan_type).bind("encrypted")
sqlx::query("insert into accounts (id, email, label, plan_type, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, 'active', ?, ?)")
.bind(id).bind(email).bind(label).bind(plan_type).bind("access-token")
.bind("2026-06-11T00:00:00Z").bind("2026-06-11T00:00:00Z")
.execute(pool).await.unwrap();
sqlx::query("insert into account_usage (account_id, request_count, empty_response_count, input_tokens, output_tokens, cached_tokens, last_used_at) values (?, ?, ?, ?, ?, ?, ?)")
@@ -205,7 +205,7 @@ async fn admin_accounts_test_app_with_overrides(
config.auth.oauth_token_endpoint = oauth_token_endpoint;
}
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box.clone()),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -224,8 +224,8 @@ async fn admin_accounts_test_app_with_overrides(
(app, state, pool, dir, secret_box)
}
async fn seed_encrypted_account(pool: &SqlitePool, secret_box: SecretBox, account: NewAccount) {
SqliteAccountStore::new(pool.clone(), secret_box)
async fn seed_account(pool: &SqlitePool, account: NewAccount) {
SqliteAccountStore::new(pool.clone())
.insert(account)
.await
.unwrap();
+12 -15
View File
@@ -11,7 +11,7 @@ async fn admin_usage_stats_should_return_page_and_summary() {
let url = format!("sqlite://{}", db.display());
let pool = connect_sqlite(&url).await.unwrap();
seed_admin_session(&pool, "session_1").await;
sqlx::query("insert into accounts (id, email, label, plan_type, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?)")
sqlx::query("insert into accounts (id, email, label, plan_type, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?)")
.bind("acct_usage").bind("usage@example.com").bind("primary").bind("plus")
.bind("cipher").bind("active").bind("2026-06-18T00:00:00Z").bind("2026-06-18T00:00:00Z")
.execute(&pool).await.unwrap();
@@ -21,9 +21,9 @@ async fn admin_usage_stats_should_return_page_and_summary() {
let secret_box = SecretBox::new([73u8; 32]);
let hasher = ApiKeyHasher::new([74u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([73u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -86,9 +86,9 @@ async fn admin_usage_stats_should_require_admin_session_cookie() {
let secret_box = SecretBox::new([125u8; 32]);
let hasher = ApiKeyHasher::new([126u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([125u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -157,9 +157,9 @@ async fn admin_usage_stats_should_cursor_page_account_usage() {
let secret_box = SecretBox::new([127u8; 32]);
let hasher = ApiKeyHasher::new([128u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([127u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -235,9 +235,8 @@ async fn admin_account_quota_should_send_usage_cookie() {
format!("{}/backend-api", server.uri()),
)
.await;
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_quota_cookie".to_string(),
email: Some("quota-cookie@example.com".to_string()),
@@ -299,9 +298,8 @@ async fn admin_account_health_check_should_send_probe_cookie() {
format!("{}/backend-api", server.uri()),
)
.await;
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_health_cookie".to_string(),
email: Some("health-cookie@example.com".to_string()),
@@ -358,9 +356,8 @@ async fn admin_account_quota_warnings_should_return_threshold_matches_from_cache
let pool = connect_sqlite(&url).await.unwrap();
seed_admin_session(&pool, "session_1").await;
let secret_box = SecretBox::new([89u8; 32]);
seed_encrypted_account(
seed_account(
&pool,
secret_box.clone(),
NewAccount {
id: "acct_warn".to_string(),
email: Some("warn@example.com".to_string()),
@@ -418,9 +415,9 @@ async fn admin_account_quota_warnings_should_return_threshold_matches_from_cache
let config = test_config(url);
let hasher = ApiKeyHasher::new([90u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([89u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
+6 -7
View File
@@ -5,10 +5,9 @@ use wiremock::{
Mock, MockServer, ResponseTemplate,
};
async fn seed_test_account(pool: &SqlitePool, secret_box: SecretBox) {
seed_encrypted_account(
async fn seed_test_account(pool: &SqlitePool) {
seed_account(
pool,
secret_box,
NewAccount {
id: "acct_test".to_string(),
email: Some("test@example.com".to_string()),
@@ -53,13 +52,13 @@ async fn account_test_models_should_return_upstream_models_only() {
})))
.mount(&server)
.await;
let (app, _state, pool, _dir, secret_box) = admin_accounts_test_app_with_api_base_url(
let (app, _state, pool, _dir, _secret_box) = admin_accounts_test_app_with_api_base_url(
"admin-account-test-models.sqlite",
91,
format!("{}/backend-api", server.uri()),
)
.await;
seed_test_account(&pool, secret_box).await;
seed_test_account(&pool).await;
let response = app
.oneshot(
@@ -125,13 +124,13 @@ async fn account_test_stream_should_translate_upstream_responses_sse() {
)
.mount(&server)
.await;
let (app, _state, pool, _dir, secret_box) = admin_accounts_test_app_with_api_base_url(
let (app, _state, pool, _dir, _secret_box) = admin_accounts_test_app_with_api_base_url(
"admin-account-test-stream.sqlite",
92,
format!("{}/backend-api", server.uri()),
)
.await;
seed_test_account(&pool, secret_box).await;
seed_test_account(&pool).await;
let response = app
.oneshot(
+2 -8
View File
@@ -24,10 +24,7 @@ async fn admin_login_should_issue_http_only_session_cookie() {
let secret_box = SecretBox::new([121u8; 32]);
let hasher = ApiKeyHasher::new([122u8; 32]);
let stores = codex_proxy_rs::runtime::services::BackgroundTaskStores {
accounts: codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(
pool.clone(),
secret_box.clone(),
),
accounts: codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(pool.clone()),
admin_sessions: codex_proxy_rs::admin::auth::service::SqliteAdminSessionStore::new(
pool.clone(),
),
@@ -108,10 +105,7 @@ async fn admin_login_should_reject_client_api_key_as_password_or_authorization()
let secret_box = SecretBox::new([123u8; 32]);
let hasher = ApiKeyHasher::new([124u8; 32]);
let stores = codex_proxy_rs::runtime::services::BackgroundTaskStores {
accounts: codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(
pool.clone(),
secret_box.clone(),
),
accounts: codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(pool.clone()),
admin_sessions: codex_proxy_rs::admin::auth::service::SqliteAdminSessionStore::new(
pool.clone(),
),
+4 -4
View File
@@ -42,9 +42,9 @@ async fn admin_login_should_issue_http_only_session_cookie() {
let secret_box = SecretBox::new([121u8; 32]);
let hasher = ApiKeyHasher::new([122u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([121u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -109,9 +109,9 @@ async fn admin_login_should_reject_client_api_key_as_password_or_authorization()
let secret_box = SecretBox::new([123u8; 32]);
let hasher = ApiKeyHasher::new([124u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([123u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
+1 -1
View File
@@ -41,7 +41,7 @@ async fn admin_client_key_test_app(db_name: &str) -> (axum::Router, tempfile::Te
let secret_box = SecretBox::new([43u8; 32]);
let hasher = ApiKeyHasher::new([44u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
+6 -7
View File
@@ -11,13 +11,12 @@ async fn client_key_store_should_create_list_disable_enable_and_delete_keys() {
assert_eq!(created.name, "cursor");
assert!(created.key.starts_with("sk_"));
let key_cipher: (String,) =
sqlx::query_as("select key_cipher from client_api_keys where id = ?")
.bind(&created.id)
.fetch_one(store.pool())
.await
.unwrap();
assert_ne!(key_cipher.0, created.key);
let key: (String,) = sqlx::query_as("select key from client_api_keys where id = ?")
.bind(&created.id)
.fetch_one(store.pool())
.await
.unwrap();
assert_eq!(key.0, created.key);
assert!(store.verify_and_touch(&created.key).await.unwrap());
+3 -3
View File
@@ -47,9 +47,9 @@ async fn admin_refresh_models_should_require_admin_session() {
let secret_box = SecretBox::new([81u8; 32]);
let hasher = ApiKeyHasher::new([82u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([81u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -106,7 +106,7 @@ async fn admin_refresh_models_should_store_snapshots_for_distinct_active_plans()
let secret_box = SecretBox::new([83u8; 32]);
let config = test_config(url, upstream.uri());
let hasher = ApiKeyHasher::new([84u8; 32]);
let store = SqliteAccountStore::new(pool.clone(), secret_box.clone());
let store = SqliteAccountStore::new(pool.clone());
store
.insert(NewAccount {
id: "acct_plus".to_string(),
+2 -2
View File
@@ -283,7 +283,7 @@ async fn dashboard_test_app(
let secret_box = SecretBox::new([73u8; 32]);
let hasher = ApiKeyHasher::new([74u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -357,7 +357,7 @@ async fn seed_usage_summary_with_last_used(
last_used_at: &str,
) {
sqlx::query(
"insert into accounts (id, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?)",
"insert into accounts (id, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?)",
)
.bind("acct_usage")
.bind("cipher")
+2 -2
View File
@@ -261,9 +261,9 @@ async fn admin_logs_test_app(
let secret_box = SecretBox::new([73u8; 32]);
let hasher = ApiKeyHasher::new([74u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([73u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
+6 -6
View File
@@ -93,9 +93,9 @@ async fn admin_settings_update_should_persist_retained_fields_to_config_yaml() {
let secret_box = SecretBox::new([53u8; 32]);
let hasher = ApiKeyHasher::new([54u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([53u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -183,9 +183,9 @@ async fn admin_settings_update_should_reject_unsupported_or_invalid_fields() {
let secret_box = SecretBox::new([53u8; 32]);
let hasher = ApiKeyHasher::new([54u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([53u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -228,9 +228,9 @@ async fn admin_settings_test_app(db_name: &str) -> (axum::Router, tempfile::Temp
let secret_box = SecretBox::new([53u8; 32]);
let hasher = ApiKeyHasher::new([54u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([53u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
+1 -1
View File
@@ -50,7 +50,7 @@ impl Write for SharedLogWriter {
}
}
#[tokio::test]
#[tokio::test(flavor = "current_thread")]
async fn http_trace_should_include_request_id_and_completion_fields() {
let logs = SharedLogBuffer::default();
let subscriber = tracing_subscriber::fmt()
+1 -4
View File
@@ -44,10 +44,7 @@ async fn server_router_should_serve_frontend_assets_without_shadowing_api_routes
let secret_box = SecretBox::new([61u8; 32]);
let hasher = ApiKeyHasher::new([62u8; 32]);
let stores = codex_proxy_rs::runtime::services::BackgroundTaskStores {
accounts: codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(
pool.clone(),
secret_box.clone(),
),
accounts: codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(pool.clone()),
admin_sessions: codex_proxy_rs::admin::auth::service::SqliteAdminSessionStore::new(
pool.clone(),
),
+7 -7
View File
@@ -118,13 +118,13 @@ async fn sqlite_schema_should_enforce_unique_chatgpt_identity() {
let pool = connect_sqlite(&url).await.unwrap();
sqlx::query(
"insert into accounts (id, chatgpt_account_id, chatgpt_user_id, access_token_cipher, status, added_at, updated_at) values ('acct_a', 'chatgpt-account', 'chatgpt-user', 'cipher', 'active', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
"insert into accounts (id, chatgpt_account_id, chatgpt_user_id, access_token, status, added_at, updated_at) values ('acct_a', 'chatgpt-account', 'chatgpt-user', 'access-token-a', 'active', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
)
.execute(&pool)
.await
.unwrap();
let result = sqlx::query(
"insert into accounts (id, chatgpt_account_id, chatgpt_user_id, access_token_cipher, status, added_at, updated_at) values ('acct_b', 'chatgpt-account', 'chatgpt-user', 'cipher', 'active', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
"insert into accounts (id, chatgpt_account_id, chatgpt_user_id, access_token, status, added_at, updated_at) values ('acct_b', 'chatgpt-account', 'chatgpt-user', 'access-token-b', 'active', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
)
.execute(&pool)
.await;
@@ -233,7 +233,7 @@ async fn sqlite_schema_should_reject_invalid_account_status() {
let pool = connect_sqlite(&url).await.unwrap();
let result = sqlx::query(
"insert into accounts (id, access_token_cipher, status, added_at, updated_at) values ('acct_bad', 'cipher', 'unknown', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
"insert into accounts (id, access_token, status, added_at, updated_at) values ('acct_bad', 'access-token', 'unknown', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
)
.execute(&pool)
.await;
@@ -265,17 +265,17 @@ async fn sqlite_schema_should_reject_non_boolean_flags() {
let pool = connect_sqlite(&url).await.unwrap();
let api_key_result = sqlx::query(
"insert into client_api_keys (id, name, prefix, key_hash, key_cipher, enabled, created_at) values ('key_bad', 'bad', 'sk_x', 'hash', 'cipher', 2, '2026-06-14T00:00:00Z')",
"insert into client_api_keys (id, name, prefix, key_hash, key, enabled, created_at) values ('key_bad', 'bad', 'sk_x', 'hash', 'sk_test', 2, '2026-06-14T00:00:00Z')",
)
.execute(&pool)
.await;
let account_result = sqlx::query(
"insert into accounts (id, access_token_cipher, status, quota_limit_reached, added_at, updated_at) values ('acct_bad', 'cipher', 'active', 2, '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
"insert into accounts (id, access_token, status, quota_limit_reached, added_at, updated_at) values ('acct_bad', 'access-token', 'active', 2, '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
)
.execute(&pool)
.await;
let quota_verify_result = sqlx::query(
"insert into accounts (id, access_token_cipher, status, quota_verify_required, added_at, updated_at) values ('acct_verify_bad', 'cipher', 'active', 2, '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
"insert into accounts (id, access_token, status, quota_verify_required, added_at, updated_at) values ('acct_verify_bad', 'access-token', 'active', 2, '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
)
.execute(&pool)
.await;
@@ -293,7 +293,7 @@ async fn sqlite_schema_should_reject_negative_account_usage_counts() {
let pool = connect_sqlite(&url).await.unwrap();
sqlx::query(
"insert into accounts (id, access_token_cipher, status, added_at, updated_at) values ('acct_a', 'cipher', 'active', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
"insert into accounts (id, access_token, status, added_at, updated_at) values ('acct_a', 'access-token', 'active', '2026-06-14T00:00:00Z', '2026-06-14T00:00:00Z')",
)
.execute(&pool)
.await
+13 -53
View File
@@ -38,7 +38,6 @@ use codex_proxy_rs::{
upstream::fingerprint::FingerprintRepository,
};
use futures::{SinkExt, StreamExt};
use secrecy::SecretString;
use serde_json::{json, Value};
use sqlx::SqlitePool;
use tokio::{
@@ -245,7 +244,7 @@ fn test_app_state_with_pool_secret_api_key_hasher_and_installation_id(
installation_id: String,
) -> AppState {
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
@@ -278,7 +277,7 @@ async fn test_app_with_account_and_installation_id(
let secret_box = SecretBox::new([83u8; 32]);
let hasher = ApiKeyHasher::new([84u8; 32]);
let api_key = insert_client_api_key(&pool, &hasher).await;
insert_account(&pool, &secret_box).await;
insert_account(&pool).await;
let state = test_app_state_with_pool_secret_api_key_hasher_and_installation_id(
test_config(url, base_url),
pool,
@@ -355,7 +354,7 @@ async fn test_app_with_account_pool_config(
let secret_box = SecretBox::new([83u8; 32]);
let hasher = ApiKeyHasher::new([84u8; 32]);
let api_key = insert_client_api_key(&pool, &hasher).await;
insert_account(&pool, &secret_box).await;
insert_account(&pool).await;
let mut config = test_config(url, base_url);
configure(&mut config);
let state = test_app_state_with_pool_secret_api_key_hasher_and_installation_id(
@@ -384,7 +383,7 @@ async fn test_app_with_restored_pool_then_disabled_account(
let secret_box = SecretBox::new([83u8; 32]);
let hasher = ApiKeyHasher::new([84u8; 32]);
let api_key = insert_client_api_key(&pool, &hasher).await;
insert_account(&pool, &secret_box).await;
insert_account(&pool).await;
let state = test_app_state_with_pool_secret_api_key_hasher_and_installation_id(
test_config(url, base_url),
pool.clone(),
@@ -416,22 +415,8 @@ async fn test_app_with_two_accounts_and_affinity(
let secret_box = SecretBox::new([83u8; 32]);
let hasher = ApiKeyHasher::new([84u8; 32]);
let api_key = insert_client_api_key(&pool, &hasher).await;
insert_named_account(
&pool,
&secret_box,
"acct_a",
"access-default",
"chatgpt-default",
)
.await;
insert_named_account(
&pool,
&secret_box,
"acct_z",
"access-affinity",
"chatgpt-affinity",
)
.await;
insert_named_account(&pool, "acct_a", "access-default", "chatgpt-default").await;
insert_named_account(&pool, "acct_z", "access-affinity", "chatgpt-affinity").await;
let now = Utc::now();
SqliteSessionAffinityStore::new(pool.clone())
.upsert(
@@ -485,17 +470,9 @@ async fn test_app_with_two_accounts_and_affinity_status(
let secret_box = SecretBox::new([83u8; 32]);
let hasher = ApiKeyHasher::new([84u8; 32]);
let api_key = insert_client_api_key(&pool, &hasher).await;
insert_named_account(&pool, "acct_primary", "access-primary", "chatgpt-primary").await;
insert_named_account(
&pool,
&secret_box,
"acct_primary",
"access-primary",
"chatgpt-primary",
)
.await;
insert_named_account(
&pool,
&secret_box,
"acct_affinity",
"access-affinity",
"chatgpt-affinity",
@@ -557,17 +534,9 @@ async fn test_app_with_two_accounts(
let secret_box = SecretBox::new([83u8; 32]);
let hasher = ApiKeyHasher::new([84u8; 32]);
let api_key = insert_client_api_key(&pool, &hasher).await;
insert_named_account(&pool, "acct_primary", "access-primary", "chatgpt-primary").await;
insert_named_account(
&pool,
&secret_box,
"acct_primary",
"access-primary",
"chatgpt-primary",
)
.await;
insert_named_account(
&pool,
&secret_box,
"acct_secondary",
"access-secondary",
"chatgpt-secondary",
@@ -639,18 +608,15 @@ async fn update_admin_account_status(app: &axum::Router, account_id: &str, statu
assert_eq!(response.status(), StatusCode::OK);
}
async fn insert_account(pool: &SqlitePool, secret_box: &SecretBox) {
let access_token = secret_box
.encrypt(&SecretString::new("access-secret".to_string().into()))
.unwrap();
async fn insert_account(pool: &SqlitePool) {
sqlx::query(
"insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token_cipher, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
"insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind("acct_chat")
.bind("user@example.com")
.bind("chatgpt-account")
.bind("chatgpt-user")
.bind(access_token)
.bind("access-secret")
.bind("2100-01-01T00:00:00Z")
.bind("active")
.bind("2026-06-18T00:00:00Z")
@@ -662,18 +628,12 @@ async fn insert_account(pool: &SqlitePool, secret_box: &SecretBox) {
async fn insert_named_account(
pool: &SqlitePool,
secret_box: &SecretBox,
id: &str,
access_token_plaintext: &str,
access_token: &str,
chatgpt_account_id: &str,
) {
let access_token = secret_box
.encrypt(&SecretString::new(
access_token_plaintext.to_string().into(),
))
.unwrap();
sqlx::query(
"insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token_cipher, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
"insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(id)
.bind(format!("{id}@example.com"))
+1 -1
View File
@@ -149,7 +149,7 @@ async fn session_affinity_store_should_forget_record_by_response_id() {
async fn insert_account(pool: &sqlx::SqlitePool, id: &str) {
sqlx::query(
"insert into accounts (id, access_token_cipher, status, added_at, updated_at) values (?, 'cipher', 'active', '2026-06-18T12:00:00Z', '2026-06-18T12:00:00Z')",
"insert into accounts (id, access_token, status, added_at, updated_at) values (?, 'access-token', 'active', '2026-06-18T12:00:00Z', '2026-06-18T12:00:00Z')",
)
.bind(id)
.execute(pool)
@@ -44,7 +44,7 @@ async fn session_affinity_store_should_restore_active_records() {
async fn insert_account(pool: &sqlx::SqlitePool, id: &str) {
sqlx::query(
"insert into accounts (id, access_token_cipher, status, added_at, updated_at) values (?, 'cipher', 'active', '2026-06-18T12:00:00Z', '2026-06-18T12:00:00Z')",
"insert into accounts (id, access_token, status, added_at, updated_at) values (?, 'access-token', 'active', '2026-06-18T12:00:00Z', '2026-06-18T12:00:00Z')",
)
.bind(id)
.execute(pool)
+4 -4
View File
@@ -35,9 +35,9 @@ async fn models_route_should_reject_unknown_client_api_key() {
let secret_box = SecretBox::new([91u8; 32]);
let hasher = ApiKeyHasher::new([92u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([91u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
@@ -77,9 +77,9 @@ async fn models_route_should_accept_stored_client_api_key() {
let config = test_config(url);
let secret_box = SecretBox::new([94u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([94u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
+2 -2
View File
@@ -144,9 +144,9 @@ async fn test_app_with_client_api_key() -> (axum::Router, String, tempfile::Temp
let config = test_config(url);
let secret_box = SecretBox::new([96u8; 32]);
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), SecretBox::new([96u8; 32])),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
session_affinity: SqliteSessionAffinityStore::new(pool.clone()),
refresh_leases: RefreshLeaseStore::new(pool.clone()),
+8 -16
View File
@@ -1,24 +1,19 @@
use std::sync::Arc;
use chrono::Utc;
use codex_proxy_rs::{
infra::crypto::SecretBox,
upstream::accounts::{
pool::{AccountAcquireRequest, AccountPoolOptions, RuntimeAccountPoolService},
store::{AccountStore, SqliteAccountStore},
},
use codex_proxy_rs::upstream::accounts::{
pool::{AccountAcquireRequest, AccountPoolOptions, RuntimeAccountPoolService},
store::{AccountStore, SqliteAccountStore},
};
use secrecy::SecretString;
use crate::support::sqlite::init_test_db;
#[tokio::test]
async fn runtime_account_pool_should_restore_capacity_and_clear_runtime_state() {
let (pool, _dir) = init_test_db("runtime-account-pool.sqlite").await;
let secret_box = SecretBox::new([34u8; 32]);
insert_account(&pool, &secret_box, "acct_pool").await;
insert_account(&pool, "acct_pool").await;
let store = SqliteAccountStore::new(pool, secret_box);
let store = SqliteAccountStore::new(pool);
let service = RuntimeAccountPoolService::new(
Arc::new(store) as Arc<dyn AccountStore>,
AccountPoolOptions {
@@ -50,18 +45,15 @@ async fn runtime_account_pool_should_restore_capacity_and_clear_runtime_state()
assert_eq!(cleared_capacity.total_slots, 0);
}
async fn insert_account(pool: &sqlx::SqlitePool, secret_box: &SecretBox, id: &str) {
let access_token = secret_box
.encrypt(&SecretString::new(format!("access-{id}").into()))
.expect("access token should encrypt");
async fn insert_account(pool: &sqlx::SqlitePool, id: &str) {
sqlx::query(
"insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token_cipher, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
"insert into accounts (id, email, chatgpt_account_id, chatgpt_user_id, access_token, access_token_expires_at, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(id)
.bind(format!("{id}@example.com"))
.bind(format!("chatgpt-{id}"))
.bind(format!("user-{id}"))
.bind(access_token)
.bind(format!("access-{id}"))
.bind("2999-01-01T00:00:00Z")
.bind("active")
.bind("2026-06-18T00:00:00Z")
+1 -1
View File
@@ -32,7 +32,7 @@ async fn services_try_new_should_use_configured_tls_transport_builder() {
let hasher = ApiKeyHasher::load_or_create(temp_dir.path().join("api-key-pepper.key"))
.expect("api key hasher");
let stores = BackgroundTaskStores {
accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()),
accounts: SqliteAccountStore::new(pool.clone()),
admin_sessions: SqliteAdminSessionStore::new(pool.clone()),
cookies: SqliteCookieStore::new(pool.clone(), secret_box),
fingerprints: FingerprintRepository::new(pool.clone()),
+2 -2
View File
@@ -154,10 +154,10 @@ async fn wait_for_appcast_requests(server: &MockServer, expected_count: usize) {
async fn insert_account(pool: &sqlx::SqlitePool, account_id: &str) {
let now = Utc::now().to_rfc3339();
sqlx::query(
"insert into accounts (id, access_token_cipher, status, added_at, updated_at) values (?, ?, 'active', ?, ?)",
"insert into accounts (id, access_token, status, added_at, updated_at) values (?, ?, 'active', ?, ?)",
)
.bind(account_id)
.bind("cipher")
.bind("access-token")
.bind(&now)
.bind(&now)
.execute(pool)
@@ -83,10 +83,7 @@ fn acquire_should_skip_accounts_with_expired_jwt_when_metadata_is_missing() {
async fn runtime_account_pool_should_persist_expired_status_when_jwt_expiry_is_discovered() {
let (pool, _dir) =
crate::support::sqlite::init_test_db("runtime-pool-expired-status.sqlite").await;
let store = codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(
pool.clone(),
codex_proxy_rs::infra::crypto::SecretBox::new([91u8; 32]),
);
let store = codex_proxy_rs::upstream::accounts::store::SqliteAccountStore::new(pool.clone());
store
.insert(codex_proxy_rs::upstream::accounts::store::NewAccount {
id: "acct_expired".to_string(),
+45 -55
View File
@@ -1,6 +1,6 @@
use chrono::{DateTime, Duration, Utc};
use codex_proxy_rs::{
infra::{crypto::SecretBox, database::connect_sqlite},
infra::database::connect_sqlite,
upstream::accounts::{
model::{AccountStatus, AccountUsageDelta},
store::{AccountStore, NewAccount, SqliteAccountStore},
@@ -9,9 +9,9 @@ use codex_proxy_rs::{
use secrecy::{ExposeSecret, SecretString};
#[tokio::test]
async fn account_repository_should_encrypt_tokens_and_load_decrypted_account() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 4).await;
let repo = SqliteAccountStore::new(pool.clone(), secret_box);
async fn account_repository_should_store_plain_tokens_and_load_secret_wrappers() {
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 4).await;
let repo = SqliteAccountStore::new(pool.clone());
let expires_at = Utc::now() + Duration::hours(1);
repo.insert(NewAccount {
@@ -30,17 +30,14 @@ async fn account_repository_should_encrypt_tokens_and_load_decrypted_account() {
.await
.unwrap();
let stored: (String, String) = sqlx::query_as(
"select access_token_cipher, refresh_token_cipher from accounts where id = ?",
)
.bind("acct_a")
.fetch_one(&pool)
.await
.unwrap();
assert!(stored.0.starts_with("v1:"));
assert!(!stored.0.contains("access-secret"));
assert!(stored.1.starts_with("v1:"));
assert!(!stored.1.contains("refresh-secret"));
let stored: (String, String) =
sqlx::query_as("select access_token, refresh_token from accounts where id = ?")
.bind("acct_a")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(stored.0, "access-secret");
assert_eq!(stored.1, "refresh-secret");
let loaded = repo.get("acct_a").await.unwrap().unwrap();
assert_eq!(loaded.access_token.expose_secret(), "access-secret");
@@ -54,12 +51,12 @@ async fn account_repository_should_encrypt_tokens_and_load_decrypted_account() {
#[tokio::test]
async fn account_repository_should_cursor_page_accounts_by_added_at_desc() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 5).await;
let repo = SqliteAccountStore::new(pool.clone(), secret_box.clone());
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 5).await;
let repo = SqliteAccountStore::new(pool.clone());
seed_repo_account(&pool, &secret_box, "acct_a", "2026-06-11T00:00:00Z").await;
seed_repo_account(&pool, &secret_box, "acct_b", "2026-06-11T00:01:00Z").await;
seed_repo_account(&pool, &secret_box, "acct_c", "2026-06-11T00:02:00Z").await;
seed_repo_account(&pool, "acct_a", "2026-06-11T00:00:00Z").await;
seed_repo_account(&pool, "acct_b", "2026-06-11T00:01:00Z").await;
seed_repo_account(&pool, "acct_c", "2026-06-11T00:02:00Z").await;
let first_page = repo.list(None, 2).await.unwrap();
assert_eq!(
@@ -83,15 +80,15 @@ async fn account_repository_should_cursor_page_accounts_by_added_at_desc() {
}
#[tokio::test]
async fn account_repository_should_list_metadata_without_decrypting_tokens() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 35).await;
let repo = SqliteAccountStore::new(pool.clone(), secret_box);
async fn account_repository_should_list_metadata_without_exposing_tokens() {
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 35).await;
let repo = SqliteAccountStore::new(pool.clone());
sqlx::query(
"insert into accounts (id, email, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)",
"insert into accounts (id, email, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?, ?)",
)
.bind("acct_corrupt")
.bind("acct_plain")
.bind("user@example.com")
.bind("not-a-secret-box-cipher")
.bind("plain-access-token")
.bind("active")
.bind("2026-06-11T00:00:00Z")
.bind("2026-06-11T00:00:00Z")
@@ -101,15 +98,15 @@ async fn account_repository_should_list_metadata_without_decrypting_tokens() {
let page = repo.list_metadata(None, 10).await.unwrap();
assert_eq!(page.items[0].id, "acct_corrupt");
assert_eq!(page.items[0].id, "acct_plain");
assert_eq!(page.items[0].email.as_deref(), Some("user@example.com"));
assert_eq!(page.items[0].status, AccountStatus::Active);
}
#[tokio::test]
async fn account_repository_should_update_status_and_label_without_rewriting_tokens() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 6).await;
let repo = SqliteAccountStore::new(pool.clone(), secret_box);
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 6).await;
let repo = SqliteAccountStore::new(pool.clone());
repo.insert(NewAccount {
id: "acct_a".to_string(),
@@ -126,7 +123,7 @@ async fn account_repository_should_update_status_and_label_without_rewriting_tok
})
.await
.unwrap();
let before: (String,) = sqlx::query_as("select access_token_cipher from accounts where id = ?")
let before: (String,) = sqlx::query_as("select access_token from accounts where id = ?")
.bind("acct_a")
.fetch_one(&pool)
.await
@@ -141,7 +138,7 @@ async fn account_repository_should_update_status_and_label_without_rewriting_tok
.await
.unwrap());
let after: (String,) = sqlx::query_as("select access_token_cipher from accounts where id = ?")
let after: (String,) = sqlx::query_as("select access_token from accounts where id = ?")
.bind("acct_a")
.fetch_one(&pool)
.await
@@ -160,8 +157,8 @@ async fn account_repository_should_update_status_and_label_without_rewriting_tok
#[tokio::test]
async fn account_repository_should_accumulate_usage_counters() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 7).await;
let repo = SqliteAccountStore::new(pool, secret_box);
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 7).await;
let repo = SqliteAccountStore::new(pool);
seed_new_account(&repo, "acct_a").await;
AccountStore::record_usage_delta(
@@ -210,12 +207,12 @@ async fn account_repository_should_accumulate_usage_counters() {
#[tokio::test]
async fn account_repository_should_restore_window_usage_into_runtime_pool_accounts() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 33).await;
let repo = SqliteAccountStore::new(pool.clone(), secret_box.clone());
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 33).await;
let repo = SqliteAccountStore::new(pool.clone());
let window_started_at = Utc::now() - Duration::minutes(2);
let window_reset_at = Utc::now() + Duration::minutes(3);
seed_repo_account(&pool, &secret_box, "acct_a", "2026-06-11T00:00:00Z").await;
seed_repo_account(&pool, "acct_a", "2026-06-11T00:00:00Z").await;
sqlx::query(
"insert into account_usage (account_id, request_count, image_input_tokens, image_output_tokens, image_request_count, image_request_failed_count, window_request_count, window_input_tokens, window_output_tokens, window_cached_tokens, window_image_input_tokens, window_image_output_tokens, window_image_request_count, window_image_request_failed_count, window_started_at, window_reset_at, limit_window_seconds) values (?, 9, 31, 9, 2, 1, 7, 11, 13, 17, 19, 5, 1, 1, ?, ?, 300)",
)
@@ -250,10 +247,10 @@ async fn account_repository_should_restore_window_usage_into_runtime_pool_accoun
#[tokio::test]
async fn account_repository_should_restore_quota_verify_required_into_runtime_pool_accounts() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 34).await;
let repo = SqliteAccountStore::new(pool.clone(), secret_box.clone());
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 34).await;
let repo = SqliteAccountStore::new(pool.clone());
seed_repo_account(&pool, &secret_box, "acct_a", "2026-06-11T00:00:00Z").await;
seed_repo_account(&pool, "acct_a", "2026-06-11T00:00:00Z").await;
sqlx::query("update accounts set quota_verify_required = 1 where id = ?")
.bind("acct_a")
.execute(&pool)
@@ -270,9 +267,9 @@ async fn account_repository_should_restore_quota_verify_required_into_runtime_po
#[tokio::test]
async fn account_repository_should_update_quota_json_and_fetched_at() {
let (pool, secret_box, _dir) = sqlite_account_store_parts("accounts.sqlite", 36).await;
let repo = SqliteAccountStore::new(pool.clone(), secret_box.clone());
seed_repo_account(&pool, &secret_box, "acct_a", "2026-06-11T00:00:00Z").await;
let (pool, _dir) = sqlite_account_store_parts("accounts.sqlite", 36).await;
let repo = SqliteAccountStore::new(pool.clone());
seed_repo_account(&pool, "acct_a", "2026-06-11T00:00:00Z").await;
let updated = repo
.update_quota_json(
@@ -306,13 +303,13 @@ async fn account_repository_should_update_quota_json_and_fetched_at() {
async fn sqlite_account_store_parts(
db_name: &str,
key_byte: u8,
) -> (sqlx::SqlitePool, SecretBox, tempfile::TempDir) {
_key_byte: u8,
) -> (sqlx::SqlitePool, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join(db_name);
let url = format!("sqlite://{}", db.display());
let pool = connect_sqlite(&url).await.unwrap();
(pool, SecretBox::new([key_byte; 32]), dir)
(pool, dir)
}
async fn seed_new_account(repo: &SqliteAccountStore, id: &str) {
@@ -333,19 +330,12 @@ async fn seed_new_account(repo: &SqliteAccountStore, id: &str) {
.unwrap();
}
async fn seed_repo_account(
pool: &sqlx::SqlitePool,
secret_box: &SecretBox,
id: &str,
added_at: &str,
) {
let token = SecretString::new(format!("access-{id}").into());
let cipher = secret_box.encrypt(&token).unwrap();
async fn seed_repo_account(pool: &sqlx::SqlitePool, id: &str, added_at: &str) {
sqlx::query(
"insert into accounts (id, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?)",
"insert into accounts (id, access_token, status, added_at, updated_at) values (?, ?, ?, ?, ?)",
)
.bind(id)
.bind(cipher)
.bind(format!("access-{id}"))
.bind("active")
.bind(added_at)
.bind(added_at)
+3 -3
View File
@@ -13,7 +13,7 @@ use sqlx::SqlitePool;
#[tokio::test]
async fn cookie_store_should_cleanup_expired_cookies() {
let (pool, secret_box, _dir) = sqlite_cookie_store_parts("cookies-cleanup.sqlite").await;
seed_account(&pool, &secret_box, "acct_a").await;
seed_account(&pool, "acct_a").await;
let store = SqliteCookieStore::new(pool.clone(), secret_box);
let now = Utc::now();
@@ -68,8 +68,8 @@ async fn sqlite_cookie_store_parts(name: &str) -> (SqlitePool, SecretBox, tempfi
(pool, SecretBox::new([21u8; 32]), dir)
}
async fn seed_account(pool: &SqlitePool, secret_box: &SecretBox, id: &str) {
SqliteAccountStore::new(pool.clone(), secret_box.clone())
async fn seed_account(pool: &SqlitePool, id: &str) {
SqliteAccountStore::new(pool.clone())
.insert(NewAccount {
id: id.to_string(),
email: Some(format!("{id}@example.com")),
+3 -3
View File
@@ -205,7 +205,7 @@ async fn quota_refresh_service_should_send_usage_cookie_when_cookie_store_is_con
.await
.expect("sqlite pool");
let secret_box = SecretBox::new([14u8; 32]);
let store = SqliteAccountStore::new(pool.clone(), secret_box.clone());
let store = SqliteAccountStore::new(pool.clone());
let cookies = SqliteCookieStore::new(pool.clone(), secret_box);
insert_quota_locked_account(&store, &pool, "acct-quota-cookie", "access-token-cookie").await;
cookies
@@ -270,7 +270,7 @@ async fn quota_refresh_service_should_fetch_usage_for_quota_locked_accounts_and_
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool.clone(), SecretBox::new([12u8; 32]));
let store = SqliteAccountStore::new(pool.clone());
store
.insert(NewAccount {
id: "acct-quota".to_string(),
@@ -352,7 +352,7 @@ async fn quota_refresh_service_should_stagger_multiple_locked_account_requests()
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool.clone(), SecretBox::new([13u8; 32]));
let store = SqliteAccountStore::new(pool.clone());
insert_quota_locked_account(&store, &pool, "acct-quota-1", "access-token-1").await;
insert_quota_locked_account(&store, &pool, "acct-quota-2", "access-token-2").await;
let codex = CodexBackendClient::new(
+2 -3
View File
@@ -1,6 +1,6 @@
use chrono::{Duration, TimeZone, Utc};
use codex_proxy_rs::{
infra::{crypto::SecretBox, database::connect_sqlite},
infra::database::connect_sqlite,
upstream::accounts::{
model::AccountStatus,
store::{NewAccount, SqliteAccountStore},
@@ -16,8 +16,7 @@ async fn refresh_lease_store_should_acquire_release_and_respect_expiry() {
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let secret_box = SecretBox::new([31u8; 32]);
let accounts = SqliteAccountStore::new(pool.clone(), secret_box);
let accounts = SqliteAccountStore::new(pool.clone());
accounts
.insert(NewAccount {
id: "acct-lease".to_string(),
@@ -7,7 +7,7 @@ async fn token_refresh_task_should_mark_due_account_refreshing_before_refresher_
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([13u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 9, 0, 0).unwrap();
let new_expires_at = now + Duration::hours(1);
let new_access_token = test_jwt(new_expires_at.timestamp());
@@ -75,7 +75,7 @@ async fn token_refresh_task_should_recover_refreshing_account_after_restart() {
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([14u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 10, 0, 0).unwrap();
let old_access_token = test_jwt((now + Duration::hours(1)).timestamp());
let new_access_token = test_jwt((now + Duration::hours(2)).timestamp());
@@ -135,7 +135,7 @@ async fn token_refresh_task_should_not_retry_ambiguous_transport_failure() {
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([15u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 11, 0, 0).unwrap();
let old_access_token = test_jwt((now + Duration::seconds(30)).timestamp());
store
@@ -199,7 +199,7 @@ async fn token_refresh_task_should_delay_recovery_after_retry_exhaustion() {
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([19u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 15, 0, 0).unwrap();
let old_access_token = test_jwt((now + Duration::seconds(30)).timestamp());
let new_access_token = test_jwt((now + Duration::hours(1)).timestamp());
@@ -281,7 +281,7 @@ async fn token_refresh_task_should_not_reuse_stale_refresh_token_after_retryable
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([31u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 16, 0, 0).unwrap();
let old_access_token = test_jwt((now + Duration::seconds(30)).timestamp());
let rotated_access_token = test_jwt((now + Duration::hours(1)).timestamp());
@@ -350,7 +350,7 @@ async fn token_refresh_task_should_confirm_invalid_grant_before_expiring_account
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([17u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 13, 0, 0).unwrap();
let old_access_token = test_jwt((now + Duration::seconds(30)).timestamp());
store
@@ -10,7 +10,6 @@ use std::{
use async_trait::async_trait;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use chrono::{Duration, TimeZone, Utc};
use codex_proxy_rs::infra::crypto::SecretBox;
use codex_proxy_rs::infra::database::connect_sqlite;
use codex_proxy_rs::upstream::accounts::model::AccountStatus;
use codex_proxy_rs::upstream::accounts::store::SqliteAccountStore;
@@ -7,7 +7,7 @@ async fn token_refresh_task_should_skip_account_when_refresh_lease_is_held() {
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool.clone(), SecretBox::new([12u8; 32]));
let store = SqliteAccountStore::new(pool.clone());
let leases = RefreshLeaseStore::new(pool.clone());
let now = Utc.with_ymd_and_hms(2026, 6, 19, 8, 0, 0).unwrap();
let old_access_token = test_jwt((now + Duration::seconds(30)).timestamp());
@@ -74,7 +74,7 @@ async fn token_refresh_task_should_skip_duplicate_in_flight_refresh() {
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([18u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 14, 0, 0).unwrap();
let new_access_token = test_jwt((now + Duration::hours(1)).timestamp());
store
@@ -163,7 +163,7 @@ async fn token_refresh_task_should_schedule_future_per_account_timer_without_ref
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([20u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 16, 0, 0).unwrap();
let old_access_token = test_jwt((now + Duration::minutes(10)).timestamp());
store
@@ -210,7 +210,7 @@ async fn token_refresh_task_should_fire_per_account_timer_at_refresh_time() {
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([21u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc::now() + Duration::minutes(5);
let old_access_token = test_jwt((now + Duration::seconds(1)).timestamp());
let new_access_token = test_jwt((now + Duration::hours(1)).timestamp());
@@ -281,7 +281,7 @@ async fn token_refresh_task_should_reschedule_next_timer_after_scheduled_refresh
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([22u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc::now();
let old_expires_at = now + Duration::milliseconds(50);
let new_access_token = test_jwt((now + Duration::hours(1)).timestamp());
@@ -7,7 +7,7 @@ async fn token_refresh_task_should_persist_refreshed_access_token_and_keep_refre
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([11u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 18, 8, 0, 0).unwrap();
let new_expires_at = now + Duration::hours(1);
let new_access_token = test_jwt(new_expires_at.timestamp());
@@ -82,7 +82,7 @@ async fn token_refresh_task_should_persist_success_after_transient_transport_ret
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let store = SqliteAccountStore::new(pool, SecretBox::new([16u8; 32]));
let store = SqliteAccountStore::new(pool);
let now = Utc.with_ymd_and_hms(2026, 6, 19, 12, 0, 0).unwrap();
let new_access_token = test_jwt((now + Duration::hours(1)).timestamp());
store
+9 -18
View File
@@ -1,15 +1,11 @@
use chrono::Utc;
use codex_proxy_rs::{
infra::crypto::SecretBox,
upstream::accounts::{
use codex_proxy_rs::upstream::{
accounts::{
model::AccountStatus,
store::{AccountStore, SqliteAccountStore},
},
upstream::models::{
BackendModelEntry, ModelPlanSnapshot, ModelSnapshotStore, SqliteModelSnapshotStore,
},
models::{BackendModelEntry, ModelPlanSnapshot, ModelSnapshotStore, SqliteModelSnapshotStore},
};
use secrecy::SecretString;
#[tokio::test]
async fn model_snapshot_repository_should_replace_and_load_plan_snapshots() {
@@ -50,17 +46,12 @@ async fn sqlite_account_store_should_list_pool_accounts() {
codex_proxy_rs::infra::database::connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let secret_box = SecretBox::new([41u8; 32]);
let now = Utc::now().to_rfc3339();
let access_token_cipher = secret_box
.encrypt(&SecretString::new("access-token".to_string().into()))
.expect("encrypt access token");
let refresh_token_cipher = secret_box
.encrypt(&SecretString::new("refresh-token".to_string().into()))
.expect("encrypt refresh token");
let access_token = "access-token";
let refresh_token = "refresh-token";
sqlx::query(
"insert into accounts (
id, email, chatgpt_account_id, chatgpt_user_id, label, plan_type, access_token_cipher, refresh_token_cipher,
id, email, chatgpt_account_id, chatgpt_user_id, label, plan_type, access_token, refresh_token,
access_token_expires_at, status, added_at, updated_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
@@ -70,8 +61,8 @@ async fn sqlite_account_store_should_list_pool_accounts() {
.bind(Option::<String>::None)
.bind("primary")
.bind("plus")
.bind(&access_token_cipher)
.bind(Some(&refresh_token_cipher))
.bind(access_token)
.bind(Some(refresh_token))
.bind(Option::<String>::None)
.bind(match AccountStatus::Active {
AccountStatus::Active => "active",
@@ -83,7 +74,7 @@ async fn sqlite_account_store_should_list_pool_accounts() {
.await
.expect("insert account");
let store = SqliteAccountStore::new(pool, secret_box);
let store = SqliteAccountStore::new(pool);
let accounts = AccountStore::list_pool_accounts(&store)
.await
.expect("list accounts");