mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
refactor: store tokens and api keys plainly
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"),
|
||||
)?,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user