登录流程集成测试
这一阶段不是程序给用户干活的主流程,而是开发者用来“验收登录系统”的试车场。all.rs 和 mod.rs 像总开关,把各项测试集中跑起来。令牌、个人令牌、Bedrock Key、存储和通用认证测试,检查凭证能不能认对、存好、删干净。命令行、设备码、浏览器登录服务器测试,模拟真实登录。刷新和登出测试则盯住过期续签和退出清理,防止账号状态留下隐患。
测试框架结构
这些文件组装 login crate 的集成测试二进制文件,并注册端到端测试运行所在的套件模块。
login/tests/all.rs源码 ↗
这个文件像一本测试目录的封面页。真正的测试内容放在 tests/suite/ 下面,这里只做一件事:声明 mod suite;,告诉 Rust 编译器把那个测试套件模块也编译进当前这个集成测试程序里。这样做的好处是,项目可以把很多测试拆成多个小文件,方便整理和阅读,但对运行测试的人来说,仍然像运行一个统一的测试入口。开头的 #![allow(clippy::expect_used)] 是给代码检查工具 Clippy 的提醒:在测试里允许使用 expect。expect 通常是在出错时直接停止并显示说明,测试代码里这么做很常见,因为测试失败时我们希望立刻看到清楚的原因。
login/tests/suite/mod.rs源码 ↗
这个文件本身不写具体测试,也不处理登录逻辑。它的作用更像一本书的目录:告诉 Rust 的测试系统,这个测试套件下面有 auth_refresh、device_code_login、login_server_e2e、logout 这几个测试模块。没有它,这些测试文件可能就不会被编译进同一个测试套件里,也就可能无法一起运行。这里的“集成测试”可以理解成从外部角度检查登录系统是否真的能正常工作,比如刷新登录状态、用设备码登录、端到端登录服务器流程、退出登录等。它的重要性在于把测试入口统一起来,让维护者不用到处找测试,也让自动化测试流程更稳定。
认证基础与存储
这些测试建立较低层的认证构建块,从 token 分类和水合,到持久化后端和广泛的 auth-manager 行为。
login/src/auth/access_token_tests.rs源码 ↗
这个文件检查一个很具体但很关键的规则:同样都是访问令牌,系统要能看出它到底是“个人访问令牌”,还是“代理身份 JWT”。JWT 可以理解成一种带有点号分隔结构的身份票据,比如“header.payload.signature”。测试里给了两个例子:一个以“at-”开头,应该被当成个人访问令牌;另一个长得像三段式 JWT,应该被当成代理身份令牌。它不负责真正登录,也不访问网络,只是在开发时自动跑一遍,像质检员一样确认分类函数没有被改坏。
classifies_personal_access_tokens_by_prefix4–13 ↗
fn classifies_personal_access_tokens_by_prefix()
作用:这个测试函数确认 classify_codex_access_token 能按令牌外形把令牌分成正确类别。有人改动令牌识别规则时,它能及时提醒:别把个人令牌和 JWT 身份票据弄混。
数据流:进去的是两个写死的示例字符串:一个是“at-example”,一个是“header.payload.signature”。函数把它们交给 classify_codex_access_token 分类,然后用断言检查结果是不是预期的枚举值;如果结果不对,测试就失败;如果都对,它不产生业务输出,只表示这条规则还正常。
调用关系:它是在测试运行时由 Rust 测试框架自动调用的。它自己不做分类,而是把具体判断交给 classify_codex_access_token,然后用 assert! 检查返回值是否符合预期,起到守门员的作用。
调用图:外部调用 1 个(assert!)。
login/src/auth/personal_access_token_tests.rs源码 ↗
这个文件是测试代码,不是正式功能本身。它像搭了一个假的身份服务器,用来演练个人访问令牌的登录流程。个人访问令牌可以理解成一把长期使用的钥匙,程序拿着它去问服务器:“这把钥匙属于谁?”测试里用 wiremock 建了一个临时假服务器,规定它收到 GET 请求、路径正确、并且带着 “Bearer at-example” 这种授权头时,才返回一份用户资料。第一个测试确认程序真的把令牌放进请求里,也没有弄丢邮箱、用户 ID、账号 ID、套餐类型、FedRAMP 标记这些元数据。第二个测试故意让服务器不返回邮箱,确认程序会报错,因为没有邮箱就无法可靠识别这个用户。辅助函数 response 用来统一生成假服务器的 JSON 回答,避免两个测试重复写同一堆字段。
response11–19 ↗
fn response(email: Option<&str>) -> serde_json::Value
作用:这个小函数生成一份假的“我是谁”接口返回数据,供测试里的假服务器使用。它让测试可以只切换邮箱有没有返回,而不用每次重复写完整 JSON。
数据流:进去的是一个可有可无的邮箱地址;函数把它和固定的用户 ID、账号 ID、套餐类型、FedRAMP 标记一起塞进 JSON 对象;出来的是一份模拟服务器会返回的用户资料数据。
调用关系:它是两个测试的共同备料工具。hydrate_sends_bearer_token_and_preserves_metadata 用它生成带邮箱的正常回答,hydrate_rejects_missing_email 用它生成缺少邮箱的异常回答;它内部只把数据交给 json! 这个生成 JSON 的工具。
调用图:被 2 处调用(hydrate_rejects_missing_email, hydrate_sends_bearer_token_and_preserves_metadata);外部调用 1 个(json!)。
hydrate_sends_bearer_token_and_preserves_metadata22–50 ↗
async fn hydrate_sends_bearer_token_and_preserves_metadata()
作用:这个测试确认:程序用个人访问令牌换取用户信息时,会把令牌作为 Bearer 授权头发给服务器,并且会完整保留服务器返回的用户元数据。简单说,就是检查“钥匙有没有正确递出去,拿回来的身份信息有没有记对”。
数据流:一开始它启动一个假的 HTTP 服务器,并设置规则:只接受 GET、指定路径、并带有 authorization: Bearer at-example 的请求;然后让被测函数拿着 at-example 去访问这个假服务器;最后拿返回的认证结果和预期结果逐项比较,同时确认假服务器确实只收到了一次符合规则的请求。
调用关系:它处在个人访问令牌认证流程的正向测试位置。它先用 start、given、method、path、header、new 这些测试工具搭好假服务器和假响应,再调用 response 准备返回内容,随后驱动真正的令牌补全流程,最后用 assert_eq! 检查结果完全一致。
调用图:调用 1 个内部函数(response);外部调用 7 个(given, start, new, assert_eq!, header, method, path)。
hydrate_rejects_missing_email53–71 ↗
async fn hydrate_rejects_missing_email()
作用:这个测试确认:如果服务器返回的令牌信息里没有邮箱,程序会把它当成无效数据并报错。这样可以避免系统接受一份缺少关键身份信息的登录结果。
数据流:一开始它启动假服务器,并设置一个 GET 指定路径的响应,但响应内容里故意没有邮箱;接着让被测函数用 at-example 去补全个人访问令牌信息;结果预期不是成功,而是拿到错误,并检查错误文字里说明了解码个人访问令牌元数据失败,最后确认假服务器按预期被访问了一次。
调用关系:它是个人访问令牌认证流程的反向测试,专门演练服务器数据不完整的情况。它用 start、given、method、path、new 搭出假接口,用 response 生成缺邮箱的 JSON,再运行被测流程,最后用 assert! 检查错误信息确实指向元数据解析失败。
调用图:调用 1 个内部函数(response);外部调用 6 个(given, start, new, assert!, method, path)。
login/src/auth/storage_tests.rs源码 ↗
登录信息很敏感,也很容易出问题:有时存在普通的 auth.json 文件里,有时存在系统钥匙串里,有时还要用加密的 secrets 后端保存。这个测试文件像一套体检表,逐个检查这些保存方式是否靠谱。它会创建临时目录,避免碰到用户真实文件;用假的钥匙串,避免真的写进系统;再构造假的登录数据、假的 JWT(像身份证一样的令牌字符串),测试保存后能不能原样读回,删除时能不能把文件和钥匙串里的旧数据都清掉。它还特别检查自动模式:优先用更安全的钥匙串,钥匙串坏了再退回文件,保证登录体验和安全性都不崩。
file_storage_load_returns_auth_dot_json18–38 ↗
async fn file_storage_load_returns_auth_dot_json() -> anyhow::Result<()>
作用:测试最普通的文件保存方式:先把登录信息写进 auth.json,再确认能完整读回来。这样可以防止保存和读取格式不一致。
数据流:进去的是一个临时目录和一份包含 API Key 的 AuthDotJson 登录数据 → 测试创建 FileAuthStorage,把数据保存到该目录下的认证文件,再调用 load 读取 → 出来的是 Some(auth_dot_json),并且断言它和原始数据完全一样。
调用关系:这是由测试框架自动运行的用例。它主要驱动 FileAuthStorage::new、save 和 load,最后用 assert_eq! 检查结果;它不把工作交给本文件里的其他辅助函数。
调用图:调用 1 个内部函数(new);外部调用 3 个(now, assert_eq!, tempdir)。
file_storage_save_persists_auth_dot_json41–64 ↗
async fn file_storage_save_persists_auth_dot_json() -> anyhow::Result<()>
作用:测试 save 真的把内容落到了磁盘文件里,而不是只存在程序内存里。它直接读文件来确认保存结果。
数据流:进去的是临时目录和一份 API Key 登录数据 → save 写入 auth.json,然后测试用 try_read_auth_json 直接从文件路径读取 → 出来的是同一份 AuthDotJson,证明磁盘文件内容正确。
调用关系:测试框架运行它。它围绕 FileAuthStorage 的 save 和底层读文件能力做验证,补充上一条 load 测试,确认不是 load 自己“假装成功”。
调用图:调用 1 个内部函数(new);外部调用 3 个(now, assert_eq!, tempdir)。
file_storage_round_trips_agent_identity_auth67–94 ↗
async fn file_storage_round_trips_agent_identity_auth() -> anyhow::Result<()>
作用:测试 agent identity 这种登录方式能不能通过文件存储完整来回一趟。agent identity 可以理解为给自动化代理使用的一张身份凭证。
数据流:进去的是临时目录和一个由 jwt_with_payload 造出的假 JWT 字符串 → 测试把它放进 AuthDotJson,保存后再读取 → 出来的是同样的 agent_identity 登录数据,没有被改格式或丢字段。
调用关系:测试框架调用它。它会借助本文件的 jwt_with_payload 生成测试令牌,然后验证 FileAuthStorage 的保存和读取是否支持这种认证模式。
调用图:调用 2 个内部函数(new, jwt_with_payload);外部调用 3 个(assert_eq!, json!, tempdir)。
file_storage_round_trips_personal_access_token_auth97–115 ↗
async fn file_storage_round_trips_personal_access_token_auth() -> anyhow::Result<()>
作用:测试个人访问令牌这种登录方式可以写入文件并原样读回。个人访问令牌就是用户给程序的一串访问凭证。
数据流:进去的是临时目录和包含 personal_access_token 的 AuthDotJson → FileAuthStorage 保存它,再 load 读取 → 出来的是同一份数据,说明这种认证字段没有被漏掉。
调用关系:测试框架运行它。它直接使用 FileAuthStorage::new、save 和 load,覆盖不同于 API Key 和 agent identity 的另一种认证模式。
调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, tempdir)。
file_storage_loads_agent_identity_as_jwt118–146 ↗
async fn file_storage_loads_agent_identity_as_jwt() -> anyhow::Result<()>
作用:测试当 auth.json 里已经有 agent_identity 字段时,读取逻辑会把它当作 JWT 字符串保留下来。JWT 是一种用点号分隔的令牌,常用来携带身份信息。
数据流:进去的是临时目录和一段手写进 auth.json 的 JSON 内容 → 测试先生成假 JWT,再把带 auth_mode 和 agent_identity 的文件写到磁盘,然后调用 load → 出来的是 AuthDotJson,其中 agent_identity 字段仍然等于原始 JWT 字符串。
调用关系:测试框架运行它。它调用 jwt_with_payload 造令牌,用标准文件写入模拟旧有 auth.json,再让 FileAuthStorage 读取,检查兼容性。
调用图:调用 2 个内部函数(new, jwt_with_payload);外部调用 5 个(assert_eq!, json!, to_string_pretty, write, tempdir)。
file_storage_delete_removes_auth_file149–172 ↗
fn file_storage_delete_removes_auth_file() -> anyhow::Result<()>
作用:测试文件存储的删除功能会真的删掉 auth.json。没有这个保证,退出登录后本地还可能残留敏感凭据。
数据流:进去的是临时目录和一份 API Key 登录数据 → 先通过 create_auth_storage 创建文件模式存储并保存,再确认 auth.json 存在,随后用 FileAuthStorage 删除 → 出来的是 removed 为 true,并且 auth.json 不再存在。
调用关系:测试框架运行它。它先通过工厂函数创建存储,再直接用 FileAuthStorage 检查删除行为,验证文件模式的清理能力。
ephemeral_storage_save_load_delete_is_in_memory_only175–202 ↗
fn ephemeral_storage_save_load_delete_is_in_memory_only() -> anyhow::Result<()>
作用:测试临时存储模式只把登录信息放在内存里,不写到磁盘。这个模式适合不希望留下本地文件痕迹的场景。
数据流:进去的是临时目录和一份 API Key 登录数据 → 用 Ephemeral 模式保存、读取、删除 → 读取时能拿到数据,删除后变成 None,同时磁盘上没有 auth.json。
调用关系:测试框架运行它。它通过 create_auth_storage 选择临时模式,验证这条路径和文件、钥匙串存储都不同:只在程序内存中生效。
调用图:调用 1 个内部函数(default);外部调用 4 个(now, assert!, assert_eq!, tempdir)。
seed_secrets_backend_and_fallback_auth_file_for_delete204–223 ↗
fn seed_secrets_backend_and_fallback_auth_file_for_delete(
mock_keyring: &MockKeyringStore,
codex_home: &Path,
auth: &AuthDotJson,
) -> anyhow::Result<PathBuf>
作用:这是测试辅助函数,用来提前布置一个“既有加密凭据,又有旧 auth.json 文件”的场景。删除测试需要这种脏环境,才能确认清理是否彻底。
数据流:进去的是假钥匙串、Codex 主目录路径和一份登录数据 → 它创建 SecretsManager,把登录数据序列化成 JSON 后写进 secrets 后端,同时在 auth.json 写入 stale 这种旧内容 → 出来的是旧 auth.json 的路径,方便测试后面检查它是否被删掉。
调用关系:它被 auto_auth_storage_delete_removes_keyring_and_file、secrets_keyring_auth_storage_delete_removes_keyring_and_file 和 secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry 调用。它负责搭舞台,真正的删除动作由那些测试函数触发。
调用图:调用 1 个内部函数(new_with_keyring_store_and_namespace);被 3 处调用(auto_auth_storage_delete_removes_keyring_and_file, secrets_keyring_auth_storage_delete_removes_keyring_and_file, secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry);外部调用 5 个(new, to_path_buf, clone, to_string, write)。
seed_secrets_backend_with_auth225–242 ↗
fn seed_secrets_backend_with_auth(
mock_keyring: &MockKeyringStore,
codex_home: &Path,
auth: &AuthDotJson,
) -> anyhow::Result<()>
作用:这是测试辅助函数,用来把一份登录信息预先塞进 secrets 后端。secrets 后端可以理解为带加密文件和钥匙串口令的安全仓库。
数据流:进去的是假钥匙串、Codex 主目录路径和 AuthDotJson → 它创建 SecretsManager,把登录信息转成 JSON 字符串后保存到全局认证密钥名下 → 出来没有业务值,但测试环境里已经有了一份可读取的加密登录信息。
调用关系:它被读取相关测试复用,例如 auto_auth_storage_load_prefers_keyring_value、auto_auth_storage_load_falls_back_when_keyring_errors 和 secrets_keyring_auth_storage_load_returns_deserialized_auth。它只负责预置数据,不负责断言。
调用图:调用 1 个内部函数(new_with_keyring_store_and_namespace);被 3 处调用(auto_auth_storage_load_falls_back_when_keyring_errors, auto_auth_storage_load_prefers_keyring_value, secrets_keyring_auth_storage_load_returns_deserialized_auth);外部调用 4 个(new, to_path_buf, clone, to_string)。
assert_keyring_saved_auth_and_removed_fallback244–277 ↗
fn assert_keyring_saved_auth_and_removed_fallback(
mock_keyring: &MockKeyringStore,
codex_home: &Path,
expected: &AuthDotJson,
) -> anyhow::Result<()>
作用:这是测试辅助断言,用来确认钥匙串/secrets 保存成功,并且旧的 auth.json 回退文件已经被删掉。它把多个安全检查集中在一处,避免每个测试重复写。
数据流:进去的是假钥匙串、Codex 主目录和期望的登录数据 → 它从 SecretsManager 读出保存值,与期望 JSON 比对;再检查旧 direct keyring 键没有被用、secrets 用的钥匙串账户存在、加密文件存在、auth.json 不存在 → 出来是测试通过或报错。
调用关系:它被 auto_auth_storage_save_prefers_keyring 和 secrets_keyring_auth_storage_save_persists_and_removes_fallback_file 调用。保存动作由那些测试完成,它负责验收保存后的现场。
调用图:调用 1 个内部函数(new_with_keyring_store_and_namespace);被 2 处调用(auto_auth_storage_save_prefers_keyring, secrets_keyring_auth_storage_save_persists_and_removes_fallback_file);外部调用 7 个(new, to_path_buf, assert!, assert_eq!, compute_keyring_account, clone, to_string)。
encrypted_auth_file279–281 ↗
fn encrypted_auth_file(codex_home: &Path) -> PathBuf
作用:这是一个小工具函数,算出加密认证文件应该放在哪里。测试用它来检查 secrets 后端有没有真的创建加密文件。
数据流:进去的是 Codex 主目录路径 → 它拼出 secrets/codex_auth.age 这个路径 → 出来的是一个 PathBuf,不读写文件。
调用关系:多个钥匙串和 secrets 相关测试会用它做存在性检查。它不调用本文件其他逻辑,只提供统一路径,避免测试里硬编码多次。
调用图:外部调用 1 个(join)。
id_token_with_prefix283–307 ↗
fn id_token_with_prefix(prefix: &str) -> IdTokenInfo
作用:这是测试辅助函数,用一个前缀造出假的 ChatGPT 身份令牌信息。前缀让不同测试数据一眼能分辨来自哪里。
数据流:进去的是 prefix 字符串 → 它组装一个假的 JWT:包含 header、payload 和签名三段,并把 email、账号 ID 等字段写进 payload → 最后调用 parse_chatgpt_jwt_claims 解析,出来的是 IdTokenInfo。
调用关系:它只被 auth_with_prefix 调用。auth_with_prefix 需要完整的 TokenData,而这个函数专门负责生成其中的 id_token 部分。
调用图:调用 1 个内部函数(parse_chatgpt_jwt_claims);被 1 处调用(auth_with_prefix);外部调用 3 个(format!, json!, to_vec)。
auth_with_prefix309–324 ↗
fn auth_with_prefix(prefix: &str) -> AuthDotJson
作用:这是测试里最常用的造数据工具,用一个前缀生成一整份假的登录信息。这样每个测试都能快速拿到不混淆的认证数据。
数据流:进去的是 prefix 字符串 → 它生成 API Key、access token、refresh token、account_id,并调用 id_token_with_prefix 生成 id_token → 出来的是一份 AuthDotJson。
调用关系:很多测试都会调用它,包括自动存储、direct keyring、secrets keyring 的读写删测试。它是这些测试的数据工厂,让测试重点放在存储行为上。
调用图:调用 1 个内部函数(id_token_with_prefix);被 11 处调用(auto_auth_storage_delete_removes_keyring_and_file, auto_auth_storage_load_falls_back_when_keyring_errors, auto_auth_storage_load_prefers_keyring_value, auto_auth_storage_load_uses_file_when_keyring_empty, auto_auth_storage_save_falls_back_when_keyring_errors, auto_auth_storage_save_prefers_keyring, direct_keyring_auth_storage_delete_removes_keyring_and_file, direct_keyring_auth_storage_saves_legacy_keyring_entry, factory_uses_secrets_backend_only_when_requested, secrets_keyring_auth_storage_delete_removes_keyring_and_file (+1 more));外部调用 1 个(format!)。
jwt_with_payload326–332 ↗
fn jwt_with_payload(payload: serde_json::Value) -> String
作用:这是测试辅助函数,把任意 JSON 内容包成一个假的 JWT 字符串。它让测试可以模拟 agent identity 这类令牌。
数据流:进去的是一个 serde_json::Value,也就是一段 JSON 数据 → 它把固定 header、传入 payload 和假签名分别做 base64url 编码,再用点号拼起来 → 出来的是形如 header.payload.signature 的 JWT 字符串。
调用关系:它被 file_storage_round_trips_agent_identity_auth 和 file_storage_loads_agent_identity_as_jwt 调用。它不验证真实签名,只负责造出格式像 JWT 的测试输入。
调用图:被 2 处调用(file_storage_loads_agent_identity_as_jwt, file_storage_round_trips_agent_identity_auth);外部调用 2 个(format!, to_vec)。
secrets_keyring_auth_storage_load_returns_deserialized_auth335–356 ↗
fn secrets_keyring_auth_storage_load_returns_deserialized_auth() -> anyhow::Result<()>
作用:测试 secrets 钥匙串存储能把加密仓库里的 JSON 登录信息读出来,并还原成程序使用的 AuthDotJson。这里的“反序列化”就是把 JSON 字符串变回结构化数据。
数据流:进去的是临时目录、假钥匙串和一份期望登录数据 → seed_secrets_backend_with_auth 先把数据写入 secrets 后端,storage.load 再读取 → 出来的是 Some(expected)。
调用关系:测试框架运行它。它依赖 seed_secrets_backend_with_auth 预置安全仓库内容,然后验证 SecretsKeyringAuthStorage 的读取路径。
调用图:调用 2 个内部函数(new, seed_secrets_backend_with_auth);外部调用 4 个(new, assert_eq!, default, tempdir)。
keyring_auth_storage_compute_store_key_for_home_directory359–366 ↗
fn keyring_auth_storage_compute_store_key_for_home_directory() -> anyhow::Result<()>
作用:测试旧 direct keyring 模式给某个主目录计算出来的钥匙串键名是稳定的。键名变了,老用户以前保存的登录信息就可能读不到。
数据流:进去的是固定路径 ~/.codex → 调用 compute_store_key 计算存储键 → 出来的是 cli|940db7b1d0e4eb40,并用断言固定这个结果。
调用关系:测试框架运行它。它不依赖其他辅助函数,专门守住 compute_store_key 的兼容性。
调用图:外部调用 2 个(from, assert_eq!)。
direct_keyring_auth_storage_saves_legacy_keyring_entry369–394 ↗
fn direct_keyring_auth_storage_saves_legacy_keyring_entry() -> anyhow::Result<()>
作用:测试旧式 direct keyring 存储会把登录信息直接写进钥匙串的旧键名下。它也确认保存成功后会删掉旧 auth.json 回退文件。
数据流:进去的是临时目录、假钥匙串和由 auth_with_prefix 生成的登录数据 → 先写一个 stale 的 auth.json,再调用 direct storage 保存 → 出来是旧钥匙串键里有正确 JSON,加密 secrets 文件不存在,auth.json 被删除,并且 load 能读回同一份数据。
调用关系:测试框架运行它。它调用 auth_with_prefix 造数据,驱动 DirectKeyringAuthStorage,并用 compute_store_key 检查旧模式保存位置。
调用图:调用 2 个内部函数(new, auth_with_prefix);外部调用 6 个(new, assert!, assert_eq!, default, write, tempdir)。
direct_keyring_auth_storage_delete_removes_keyring_and_file397–425 ↗
fn direct_keyring_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()>
作用:测试旧式 direct keyring 删除时,会同时清掉钥匙串里的凭据和 auth.json 文件。这样退出登录不会留下两处残留。
数据流:进去的是临时目录、假钥匙串和一份登录数据 → 先保存到 direct keyring,再制造一个 stale 的 auth.json,然后调用 delete → 出来是 removed 为 true,load 读不到,旧钥匙串键为空,auth.json 和加密文件都不存在。
调用关系:测试框架运行它。它用 auth_with_prefix 准备数据,检查 DirectKeyringAuthStorage 的删除动作是否覆盖钥匙串和文件两个地方。
调用图:调用 2 个内部函数(new, auth_with_prefix);外部调用 6 个(new, assert!, assert_eq!, default, write, tempdir)。
factory_uses_secrets_backend_only_when_requested428–463 ↗
fn factory_uses_secrets_backend_only_when_requested() -> anyhow::Result<()>
作用:测试创建存储对象的工厂函数会尊重配置:要求 Direct 就用旧钥匙串,要求 Secrets 才用加密 secrets 后端。这样配置不会被悄悄忽略。
数据流:进去的是两组临时目录、两套假钥匙串和两份登录数据 → 第一组用 Direct 保存,检查旧键名有值且没有加密文件;第二组用 Secrets 保存,检查 secrets 的钥匙串账户有值且加密文件存在 → 出来是两种模式各走各的路。
调用关系:测试框架运行它。它调用 create_auth_storage_with_store 来验证工厂分派逻辑,并用 auth_with_prefix 生成两套可区分的数据。
调用图:调用 1 个内部函数(auth_with_prefix);外部调用 4 个(new, assert!, default, tempdir)。
secrets_keyring_auth_storage_save_persists_and_removes_fallback_file466–494 ↗
fn secrets_keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Result<()>
作用:测试 secrets 钥匙串存储保存登录信息时,会写入安全仓库,并删掉旧 auth.json。这个行为能避免同一份凭据同时散落在安全和不安全位置。
数据流:进去的是临时目录、假钥匙串、旧 auth.json 和一份 ChatGPT token 登录数据 → storage.save 把数据保存进 secrets 后端 → assert_keyring_saved_auth_and_removed_fallback 检查加密保存成功、旧 key 没用、旧文件被删。
调用关系:测试框架运行它。它自己构造 TokenData,然后把验收工作交给 assert_keyring_saved_auth_and_removed_fallback。
调用图:调用 2 个内部函数(new, assert_keyring_saved_auth_and_removed_fallback);外部调用 6 个(new, default, now, default, write, tempdir)。
secrets_keyring_auth_storage_delete_removes_keyring_and_file497–520 ↗
fn secrets_keyring_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()>
作用:测试 secrets 钥匙串存储删除时,会清掉加密登录信息和旧 auth.json 文件。它确保安全仓库模式退出登录后也没有残留。
数据流:进去的是临时目录、假钥匙串和一份登录数据 → seed_secrets_backend_and_fallback_auth_file_for_delete 先布置加密数据和旧文件,然后 delete 删除 → 出来是 removed 为 true,load 为 None,旧 auth.json 不存在。
调用关系:测试框架运行它。它借助 seed_secrets_backend_and_fallback_auth_file_for_delete 搭建删除前状态,核心验证 SecretsKeyringAuthStorage::delete。
调用图:调用 3 个内部函数(new, auth_with_prefix, seed_secrets_backend_and_fallback_auth_file_for_delete);外部调用 5 个(new, assert!, assert_eq!, default, tempdir)。
secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry523–556 ↗
fn secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry() -> anyhow::Result<()>
作用:测试从旧 direct keyring 迁到 secrets 后,删除操作也会顺手清掉旧 direct keyring 里的残留。否则老版本留下的凭据可能还躲在系统钥匙串里。
数据流:进去的是临时目录、假钥匙串、旧 direct 凭据和新 secrets 凭据 → 先用 DirectKeyringAuthStorage 保存旧数据,再用辅助函数布置 secrets 数据和旧文件,然后调用 secrets storage 删除 → 出来是 secrets 读不到、direct 也读不到、auth.json 被删。
调用关系:测试框架运行它。它同时创建 DirectKeyringAuthStorage 和 SecretsKeyringAuthStorage,验证新删除逻辑会照顾历史遗留数据。
调用图:调用 4 个内部函数(new, new, auth_with_prefix, seed_secrets_backend_and_fallback_auth_file_for_delete);外部调用 5 个(new, assert!, assert_eq!, default, tempdir)。
auto_auth_storage_load_prefers_keyring_value559–576 ↗
fn auto_auth_storage_load_prefers_keyring_value() -> anyhow::Result<()>
作用:测试自动存储读取时,如果钥匙串/secrets 里有数据,就优先用它,而不是用 auth.json。安全存储应该比普通文件优先级更高。
数据流:进去的是临时目录、假钥匙串、一份 keyring 数据和一份 file 数据 → 先把 keyring 数据塞进 secrets 后端,再把 file 数据写进文件存储,然后调用 auto storage load → 出来的是 keyring_auth。
调用关系:测试框架运行它。它调用 seed_secrets_backend_with_auth 和 auth_with_prefix 搭建两份不同来源的数据,用来验证 AutoAuthStorage 的优先级规则。
调用图:调用 3 个内部函数(new, auth_with_prefix, seed_secrets_backend_with_auth);外部调用 4 个(new, assert_eq!, default, tempdir)。
auto_auth_storage_load_uses_file_when_keyring_empty579–594 ↗
fn auto_auth_storage_load_uses_file_when_keyring_empty() -> anyhow::Result<()>
作用:测试自动存储在钥匙串为空时,会退回读取 auth.json。这样没有钥匙串数据的用户仍然能登录。
数据流:进去的是临时目录、空的假钥匙串和一份文件登录数据 → 只把数据保存到 file_storage,再调用 auto storage load → 出来的是文件里的 expected 数据。
调用关系:测试框架运行它。它不预置 secrets 后端,只让 AutoAuthStorage 面对“钥匙串没东西”的情况,确认回退到 FileAuthStorage。
调用图:调用 2 个内部函数(new, auth_with_prefix);外部调用 4 个(new, assert_eq!, default, tempdir)。
auto_auth_storage_load_falls_back_when_keyring_errors597–617 ↗
fn auto_auth_storage_load_falls_back_when_keyring_errors() -> anyhow::Result<()>
作用:测试自动存储在钥匙串读取报错时,不会直接失败,而是尝试读取 auth.json。这样系统钥匙串临时坏掉时,用户还有机会继续使用。
数据流:进去的是临时目录、假钥匙串、一个被设置为报错的钥匙串键,以及一份文件回退数据 → 先在 secrets 里放数据,再让钥匙串读取该键时报错,同时把 expected 保存到文件 → load 最终出来的是文件数据。
调用关系:测试框架运行它。它用 seed_secrets_backend_with_auth 制造钥匙串内容,再用 mock_keyring.set_error 模拟故障,验证 AutoAuthStorage 的容错路径。
调用图:调用 3 个内部函数(new, auth_with_prefix, seed_secrets_backend_with_auth);外部调用 6 个(new, Invalid, assert_eq!, compute_keyring_account, default, tempdir)。
auto_auth_storage_save_prefers_keyring620–636 ↗
fn auto_auth_storage_save_prefers_keyring() -> anyhow::Result<()>
作用:测试自动存储保存时会优先写入钥匙串/secrets,而不是继续留在 auth.json。这样新保存的凭据会进入更安全的位置。
数据流:进去的是临时目录、假钥匙串、一个旧文件凭据和一个新凭据 → 先把 stale 存到文件,再调用 auto storage save 保存 expected → 出来是 secrets 后端保存了 expected,旧 auth.json 被删除。
调用关系:测试框架运行它。它用 auth_with_prefix 造新旧两份数据,并把最终检查交给 assert_keyring_saved_auth_and_removed_fallback。
调用图:调用 3 个内部函数(new, assert_keyring_saved_auth_and_removed_fallback, auth_with_prefix);外部调用 3 个(new, default, tempdir)。
auto_auth_storage_save_falls_back_when_keyring_errors639–668 ↗
fn auto_auth_storage_save_falls_back_when_keyring_errors() -> anyhow::Result<()>
作用:测试自动存储在钥匙串保存失败时,会把登录信息保存到 auth.json。虽然不如钥匙串安全,但至少不会让登录信息丢失。
数据流:进去的是临时目录、假钥匙串和一份登录数据 → 先把 secrets 对应的钥匙串键设置成保存时报错,再调用 save → 出来是 auth.json 被创建,文件存储能读回 auth,钥匙串里没有保存失败后的脏值。
调用关系:测试框架运行它。它通过 mock_keyring.set_error 模拟钥匙串故障,验证 AutoAuthStorage 保存路径的降级策略。
调用图:调用 2 个内部函数(new, auth_with_prefix);外部调用 7 个(new, Invalid, assert!, assert_eq!, compute_keyring_account, default, tempdir)。
auto_auth_storage_delete_removes_keyring_and_file671–695 ↗
fn auto_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()>
作用:测试自动存储删除时,会同时清掉钥匙串/secrets 和 auth.json。自动模式可能同时遇到两种残留,所以删除必须一网打尽。
数据流:进去的是临时目录、假钥匙串和一份登录数据 → seed_secrets_backend_and_fallback_auth_file_for_delete 先放入加密凭据和旧 auth.json,再调用 auto storage delete → 出来是 removed 为 true,load 为 None,旧文件不存在。
调用关系:测试框架运行它。它复用删除场景辅助函数搭建现场,核心验证 AutoAuthStorage 会把 keyring 和 file 两条存储路径都清理掉。
调用图:调用 3 个内部函数(new, auth_with_prefix, seed_secrets_backend_and_fallback_auth_file_for_delete);外部调用 5 个(new, assert!, assert_eq!, default, tempdir)。
login/src/auth/auth_tests.rs源码 ↗
认证代码一旦出错,轻则用户登录不了,重则可能把错误的账号、令牌或工作区权限当成有效凭据。这个测试文件专门模拟各种登录场景:API Key、ChatGPT 令牌、个人访问令牌、Agent Identity、外部脚本提供的 bearer token 等。它会用临时目录伪造 auth.json,用 mock HTTP 服务器假装远端认证服务,还会临时改环境变量再恢复。重点检查三类事:该写入什么、不该写入什么;无效或不匹配的凭据必须失败;账号套餐和工作区信息要被正确识别。文件里还有一些小工具,用来生成假的 JWT(JSON Web Token,一种带信息的登录票据)、测试用密钥响应和环境变量保护器。
refresh_without_id_token31–60 ↗
async fn refresh_without_id_token()
作用:测试刷新令牌时,如果调用方没有提供新的 id_token,系统会保留原来的 id_token。这样不会因为一次刷新少传了字段,就把用户身份信息弄丢。
数据流:先在临时目录写入一份带假 JWT 的 auth.json → 调用 persist_tokens,只给新的 access token 和 refresh token,不给 id_token → 读回结果,确认旧 id_token 还在,新访问令牌和刷新令牌已经替换。
调用关系:它使用 write_auth_file 准备旧登录状态,然后直接验证父模块的 persist_tokens。这个测试覆盖的是令牌刷新保存这一小段关键流程。
调用图:调用 2 个内部函数(default, write_auth_file);外部调用 3 个(assert_eq!, persist_tokens, tempdir)。
login_with_api_key_overwrites_existing_auth_json63–95 ↗
fn login_with_api_key_overwrites_existing_auth_json()
作用:测试用 API Key 登录时,会清掉旧的 ChatGPT 令牌,只留下新的 API Key。这样能避免一个 auth.json 里混着两套身份。
数据流:先手写一个旧 auth.json,里面同时有旧 API Key 和旧 tokens → 调用 login_with_api_key 写入新 key → 再读取文件,确认 OPENAI_API_KEY 变成新值,tokens 被清空。
调用关系:它直接测试 login_with_api_key,并用 FileAuthStorage 读回结果。它保证 API Key 登录会覆盖旧状态,而不是在旧认证信息上叠加。
调用图:调用 2 个内部函数(default, new);外部调用 7 个(assert!, assert_eq!, json!, to_string_pretty, write, login_with_api_key, tempdir)。
login_with_access_token_writes_only_token98–136 ↗
async fn login_with_access_token_writes_only_token()
作用:测试用 Agent Identity 这种访问令牌登录时,只保存 Agent Identity,不保留 API Key 或普通 tokens。Agent Identity 可以理解成给自动代理用的一张已签名身份证。
数据流:先生成一份带允许工作区的 Agent Identity 记录并签名成 JWT → mock 服务器提供验证签名用的公钥列表 → 调用 login_with_access_token → 读 auth.json,确认模式是 AgentIdentity,只保存 agent_identity,其他凭据为空。
调用关系:它调用 agent_identity_record、signed_agent_identity_jwt 和 test_jwks_body 搭出完整验证环境,再测试 login_with_access_token 的保存行为。
调用图:调用 5 个内部函数(default, agent_identity_record, signed_agent_identity_jwt, test_jwks_body, new);外部调用 11 个(given, start, new, assert!, assert_eq!, format!, json!, login_with_access_token, tempdir, method (+1 more))。
login_with_access_token_writes_only_personal_access_token140–188 ↗
async fn login_with_access_token_writes_only_personal_access_token()
作用:测试个人访问令牌登录成功后,只保存这一个令牌,不额外写 auth_mode、API Key 或 OAuth tokens。个人访问令牌就是用户自己创建的一串长期或半长期访问凭据。
数据流:mock 认证服务返回 whoami 信息,说明令牌属于允许的工作区 → 临时设置认证服务地址环境变量 → 调用 login_with_access_token → 读 auth.json,确认只保存 personal_access_token,并且解析出的登录模式是 PersonalAccessToken。
调用关系:它用 EnvVarGuard::set 临时指向 mock 服务,用 personal_access_token_whoami 构造服务回答,验证 login_with_access_token 对个人访问令牌的分支。
调用图:调用 4 个内部函数(set, default, personal_access_token_whoami, new);外部调用 12 个(given, start, new, assert!, assert_eq!, from_str, read_to_string, login_with_access_token, tempdir, header (+2 more))。
login_with_access_token_rejects_personal_access_token_workspace_mismatch192–225 ↗
async fn login_with_access_token_rejects_personal_access_token_workspace_mismatch()
作用:测试个人访问令牌属于不允许的工作区时,登录必须失败。这样可以防止用户拿错组织或工作区的凭据混进来。
数据流:mock 服务说这个令牌属于被禁止的工作区 → 调用 login_with_access_token 并传入允许工作区列表 → 得到 PermissionDenied 错误,同时确认没有写 auth.json。
调用关系:它依赖 personal_access_token_whoami 制造“不匹配工作区”的返回值,专门检查 login_with_access_token 的工作区限制。
调用图:调用 3 个内部函数(set, default, personal_access_token_whoami);外部调用 10 个(given, start, new, assert!, assert_eq!, login_with_access_token, tempdir, header, method, path)。
login_with_access_token_rejects_invalid_personal_access_token229–257 ↗
async fn login_with_access_token_rejects_invalid_personal_access_token()
作用:测试个人访问令牌无效时,登录会失败且不会留下认证文件。无效令牌不能被悄悄保存,否则之后每次启动都可能误用。
数据流:mock 认证服务对 whoami 返回 403 → 调用 login_with_access_token → 得到错误 → 检查 auth.json 不存在。
调用关系:它用 EnvVarGuard::set 指向 mock 服务,验证 login_with_access_token 在远端拒绝令牌时的失败路径。
调用图:调用 2 个内部函数(set, default);外部调用 9 个(given, start, new, assert!, assert_eq!, login_with_access_token, tempdir, method, path)。
login_with_access_token_rejects_invalid_jwt260–279 ↗
async fn login_with_access_token_rejects_invalid_jwt()
作用:测试传入的访问令牌既不是有效个人访问令牌,也不是合法 JWT 时,系统会拒绝。JWT 是一种用点号分三段的登录票据。
数据流:把字符串 not-a-jwt 当访问令牌传进去 → login_with_access_token 解析失败 → 返回错误,并且不创建 auth.json。
调用关系:这是 login_with_access_token 最基础的输入校验测试,不依赖 mock 服务器。
调用图:调用 1 个内部函数(default);外部调用 4 个(assert!, assert_eq!, login_with_access_token, tempdir)。
login_with_access_token_rejects_unsigned_jwt282–311 ↗
async fn login_with_access_token_rejects_unsigned_jwt()
作用:测试伪造的、没有真正签名保护的 Agent Identity JWT 会被拒绝。这样能防止别人自己编一张“身份证”就登录成功。
数据流:生成一个内容像 Agent Identity、但签名不可信的 JWT → mock 服务器提供真实公钥 → 调用 login_with_access_token → 期望失败,且不写 auth.json。
调用关系:它用 fake_agent_identity_jwt 生成假票据,用 test_jwks_body 模拟公钥服务,验证 login_with_access_token 会检查签名。
调用图:调用 4 个内部函数(default, agent_identity_record, fake_agent_identity_jwt, test_jwks_body);外部调用 9 个(given, start, new, assert!, format!, login_with_access_token, tempdir, method, path)。
missing_auth_json_returns_none315–327 ↗
async fn missing_auth_json_returns_none()
作用:测试没有 auth.json 且环境变量里也没有访问令牌时,读取认证结果应该是 None。也就是说,没有登录就明确返回“没有登录”。
数据流:创建空临时目录 → 移除访问令牌环境变量 → 调用 CodexAuth::from_auth_storage → 得到 None。
调用关系:它通过 remove_access_token_env_var 清理环境影响,测试 CodexAuth::from_auth_storage 的空状态行为。
调用图:调用 3 个内部函数(default, remove_access_token_env_var, from_auth_storage);外部调用 2 个(assert_eq!, tempdir)。
pro_account_with_no_api_key_uses_chatgpt_auth331–390 ↗
async fn pro_account_with_no_api_key_uses_chatgpt_auth()
作用:测试 auth.json 里没有 API Key、但有 ChatGPT Pro 令牌时,系统会按 ChatGPT 登录处理。这样用户不需要 API Key 也能走 ChatGPT 认证。
数据流:写入一份带 pro 套餐信息的假 auth.json → 调用 load_auth → 检查没有 api_key,认证模式是 Chatgpt,用户 ID、套餐、last_refresh 和 tokens 都被正确读出。
调用关系:它使用 write_auth_file 造文件,然后验证 load_auth 对 ChatGPT token 文件的解析结果。
调用图:调用 2 个内部函数(remove_access_token_env_var, write_auth_file);外部调用 3 个(assert_eq!, load_auth, tempdir)。
loads_api_key_from_auth_json394–419 ↗
async fn loads_api_key_from_auth_json()
作用:测试 auth.json 里只有 OPENAI_API_KEY 时,系统能把它当 API Key 登录读取出来。
数据流:手写 auth.json,放入 sk-test-key → 清掉访问令牌环境变量 → 调用 load_auth → 得到 ApiKey 模式,并能读到这个 key;同时确认它没有 token 数据。
调用关系:它测试 load_auth 的 API Key 分支,并避免环境变量干扰。
调用图:调用 1 个内部函数(remove_access_token_env_var);外部调用 5 个(assert!, assert_eq!, write, load_auth, tempdir)。
logout_removes_auth_file422–448 ↗
fn logout_removes_auth_file() -> Result<(), std::io::Error>
作用:测试登出会删除 auth.json。这样本地不会继续保存旧凭据。
数据流:先调用 save_auth 写入一份 API Key 登录文件 → 确认文件存在 → 调用 logout → 确认返回 true 且文件被删除。
调用关系:它把 save_auth 和 logout 连起来测试,确认保存后的认证状态能被正常清除。
调用图:调用 2 个内部函数(default, get_auth_file);外部调用 3 个(assert!, save_auth, tempdir)。
unauthorized_recovery_reports_mode_and_step_names452–479 ↗
async fn unauthorized_recovery_reports_mode_and_step_names()
作用:测试未授权恢复流程能说清自己处于什么模式、哪一步。这里的恢复流程指请求遇到 401 未授权后,尝试重新加载或刷新认证。
数据流:创建 AuthManager → 手动构造 managed 和 external 两种 UnauthorizedRecovery → 分别读取 mode_name 和 step_name → 确认返回稳定的字符串。
调用关系:它用 AuthManager::shared 提供管理器,只检查 UnauthorizedRecovery 的命名输出,方便日志或上层逻辑识别状态。
调用图:调用 2 个内部函数(default, shared);外部调用 3 个(clone, assert_eq!, tempdir)。
refresh_failure_is_scoped_to_the_matching_auth_snapshot483–535 ↗
async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot()
作用:测试一次永久刷新失败只绑定到当时那份认证快照,不会误伤后来已经换过令牌的新认证。
数据流:先加载一份 ChatGPT 认证 → 复制并改出一份 access token、refresh token 不同的新认证 → 在 manager 里记录旧认证的刷新失败 → 查询旧认证有错误,新认证没有错误。
调用关系:它用 write_auth_file 和 load_auth 准备旧状态,再用 CodexAuth::from_auth_dot_json 造新状态,测试 AuthManager 的失败记录范围。
调用图:调用 5 个内部函数(remove_access_token_env_var, write_auth_file, from_auth_for_testing, from_auth_dot_json, new);外部调用 3 个(assert_eq!, load_auth, tempdir)。
external_auth_tokens_without_chatgpt_metadata_cannot_seed_chatgpt_auth538–548 ↗
fn external_auth_tokens_without_chatgpt_metadata_cannot_seed_chatgpt_auth()
作用:测试外部只给一个 bearer token、没有 ChatGPT 用户信息时,不能拿来初始化 ChatGPT 登录。bearer token 就是请求头里“Bearer xxx”的那串通行证。
数据流:构造只有 access token 的 ExternalAuthTokens → 调用 AuthDotJson::from_external_tokens → 期望报错,错误信息说明缺少 ChatGPT 元数据。
调用关系:它直接验证 AuthDotJson::from_external_tokens 的防呆逻辑,避免把信息不足的外部令牌当完整 ChatGPT 账号。
调用图:调用 1 个内部函数(access_token_only);外部调用 2 个(assert_eq!, from_external_tokens)。
external_bearer_only_auth_manager_uses_cached_provider_token551–568 ↗
async fn external_bearer_only_auth_manager_uses_cached_provider_token()
作用:测试外部脚本提供 token 时,AuthManager 会缓存第一次拿到的 token,而不是每次都运行脚本换新值。
数据流:创建一个脚本,第一次输出 provider-token,第二次会输出 next-token → 用它创建 external_bearer_only 管理器 → 连续取两次 auth → 两次都得到 provider-token,并确认模式是 ApiKey。
调用关系:它依赖 ProviderAuthScript::new 生成测试脚本,验证 AuthManager::external_bearer_only 的缓存行为。
调用图:调用 2 个内部函数(new, external_bearer_only);外部调用 1 个(assert_eq!)。
external_bearer_only_auth_manager_disables_auto_refresh_when_interval_is_zero571–588 ↗
async fn external_bearer_only_auth_manager_disables_auto_refresh_when_interval_is_zero()
作用:测试外部 bearer token 的自动刷新间隔设为 0 时,不会自动刷新。这样配置明确表示“只用当前缓存”。
数据流:创建会依次输出两个 token 的脚本 → 把 refresh_interval_ms 改成 0 → 创建管理器并取两次认证 → 两次仍然是第一个 token。
调用关系:它用 ProviderAuthScript::new 和 AuthManager::external_bearer_only 检查刷新配置对缓存策略的影响。
调用图:调用 2 个内部函数(new, external_bearer_only);外部调用 1 个(assert_eq!)。
external_bearer_only_auth_manager_returns_none_when_command_fails591–596 ↗
async fn external_bearer_only_auth_manager_returns_none_when_command_fails()
作用:测试外部取 token 的命令失败时,认证结果是 None,而不是伪造一个错误凭据。
数据流:创建一个总是退出失败的脚本 → 用它创建 external_bearer_only 管理器 → 调用 auth → 得到 None。
调用关系:它通过 ProviderAuthScript::new_failing 制造失败命令,覆盖 AuthManager 读取外部 token 的异常路径。
调用图:调用 2 个内部函数(new_failing, external_bearer_only);外部调用 1 个(assert_eq!)。
unauthorized_recovery_uses_external_refresh_for_bearer_manager599–626 ↗
async fn unauthorized_recovery_uses_external_refresh_for_bearer_manager()
作用:测试外部 bearer token 管理器遇到未授权恢复时,会重新运行外部命令拿新 token。
数据流:脚本第一次输出 provider-token,第二次输出 refreshed-provider-token → 管理器先读到旧 token → 调用 unauthorized_recovery 的 next → 再读认证,确认 token 已换成 refreshed-provider-token。
调用关系:它把 ProviderAuthScript、AuthManager::external_bearer_only 和 unauthorized_recovery 串起来,验证 401 后的外部刷新流程。
调用图:调用 2 个内部函数(new, external_bearer_only);外部调用 2 个(assert!, assert_eq!)。
ProviderAuthScript::new635–701 ↗
fn new(tokens: &[&str]) -> std::io::Result<Self>
作用:创建一个测试用小脚本,每运行一次就打印 token 文件的第一行,并把这一行从文件里删掉。它用来模拟“外部程序提供访问令牌”。
数据流:输入一组 token 字符串 → 创建临时目录和 tokens.txt → 按操作系统写 shell 脚本或 Windows cmd 脚本 → 返回包含目录、命令和参数的 ProviderAuthScript。
调用关系:它被多个外部 bearer token 测试调用,为 AuthManager::external_bearer_only 提供可控的外部命令。
调用图:被 3 处调用(external_bearer_only_auth_manager_disables_auto_refresh_when_interval_is_zero, external_bearer_only_auth_manager_uses_cached_provider_token, unauthorized_recovery_uses_external_refresh_for_bearer_manager);外部调用 8 个(new, new, cfg!, metadata, set_permissions, write, tempdir, vec!)。
ProviderAuthScript::new_failing703–740 ↗
fn new_failing() -> std::io::Result<Self>
作用:创建一个测试用失败脚本,运行时直接返回错误。它用来检查外部取 token 失败时系统怎么处理。
数据流:创建临时目录 → 按操作系统写一个会退出失败的脚本或命令 → 返回 ProviderAuthScript,里面保存失败命令的启动方式。
调用关系:它只被 external_bearer_only_auth_manager_returns_none_when_command_fails 使用,用来制造稳定的失败场景。
调用图:被 1 处调用(external_bearer_only_auth_manager_returns_none_when_command_fails);外部调用 6 个(new, metadata, set_permissions, write, tempdir, vec!)。
ProviderAuthScript::auth_config742–753 ↗
fn auth_config(&self) -> ModelProviderAuthInfo
作用:把测试脚本包装成 ModelProviderAuthInfo 配置,让认证管理器知道要运行哪个命令、在哪个目录运行、多久超时、多久刷新。
数据流:读取 ProviderAuthScript 里的 command、args 和临时目录 → 组装 JSON → 反序列化成 ModelProviderAuthInfo → 返回给测试使用。
调用关系:外部 bearer token 测试先创建 ProviderAuthScript,再调用 auth_config,把配置交给 AuthManager::external_bearer_only。
调用图:外部调用 2 个(json!, from_value)。
write_auth_file762–777 ↗
fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result<String>
作用:往临时目录写一份测试用 auth.json。它让很多测试不用重复手写认证文件。
数据流:输入 AuthFileParams 和 codex_home 路径 → 调用 fake_jwt_for_auth_file_params 生成假 id_token → 组装 OPENAI_API_KEY、tokens、last_refresh → 写到 auth.json → 返回生成的假 JWT。
调用关系:它是本文件最常用的测试夹具之一,被登录加载、工作区限制、套餐映射等测试反复调用。
调用图:调用 2 个内部函数(fake_jwt_for_auth_file_params, get_auth_file);被 11 处调用(enforce_login_restrictions_allows_any_matching_workspace_in_list, enforce_login_restrictions_allows_matching_workspace, enforce_login_restrictions_logs_out_for_workspace_mismatch, missing_plan_type_maps_to_unknown, plan_type_maps_enterprise_cbp_usage_based_plan, plan_type_maps_known_plan, plan_type_maps_self_serve_business_usage_based_plan, plan_type_maps_unknown_to_unknown, pro_account_with_no_api_key_uses_chatgpt_auth, refresh_failure_is_scoped_to_the_matching_auth_snapshot (+1 more));外部调用 3 个(json!, to_string_pretty, write)。
fake_jwt_for_auth_file_params779–813 ↗
fn fake_jwt_for_auth_file_params(params: &AuthFileParams) -> std::io::Result<String>
作用:根据测试参数生成一个假的 ChatGPT id_token。它不用于真实安全验证,只是让解析代码能读到邮箱、用户 ID、套餐和账号 ID。
数据流:读取 AuthFileParams 里的套餐和账号 ID → 构造 JWT 头和 payload → 用 URL 安全的 base64 编码三段内容 → 输出形如 header.payload.signature 的字符串。
调用关系:它只被 write_auth_file 调用,是伪造 auth.json 中 id_token 的内部小工具。
调用图:被 1 处调用(write_auth_file);外部调用 4 个(format!, String, json!, to_vec)。
build_config815–828 ↗
async fn build_config(
codex_home: &Path,
forced_login_method: Option<ForcedLoginMethod>,
forced_chatgpt_workspace_id: Option<Vec<String>>,
) -> AuthConfig
作用:快速生成测试用 AuthConfig。AuthConfig 可以理解成认证模块运行时需要的一张配置表。
数据流:输入 codex_home、强制登录方式、允许工作区列表 → 填入文件存储、Direct keyring、无 ChatGPT base URL 等固定测试值 → 返回 AuthConfig。
调用关系:它被多条 enforce_login_restrictions 测试使用,避免每个测试重复写同样配置。
调用图:被 6 处调用(enforce_login_restrictions_allows_any_matching_workspace_in_list, enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_forced_chatgpt_workspace_id_is_set, enforce_login_restrictions_allows_matching_workspace, enforce_login_restrictions_blocks_env_api_key_when_chatgpt_required, enforce_login_restrictions_logs_out_for_method_mismatch, enforce_login_restrictions_logs_out_for_workspace_mismatch);外部调用 1 个(to_path_buf)。
EnvVarGuard::set840–846 ↗
EnvVarGuard::remove848–854 ↗
fn remove(key: &'static str) -> Self
作用:临时删除一个环境变量,并记住它原来的值。这样测试能确保某个环境变量不会影响结果。
数据流:输入变量名 → 读取原值 → 删除这个变量 → 返回 EnvVarGuard 保存旧状态。
调用关系:remove_access_token_env_var 会调用它,很多测试借此清掉 CODEX_ACCESS_TOKEN_ENV_VAR。
调用图:外部调用 2 个(remove_var, var_os)。
EnvVarGuard::drop859–866 ↗
fn drop(&mut self)
作用:在 EnvVarGuard 离开作用域时自动恢复环境变量。它像“用完自动归还”的临时开关。
数据流:读取 guard 里保存的原值 → 如果原来有值就设回去,如果原来没有就删除 → 不返回值,只恢复进程环境。
调用关系:它由 Rust 的 Drop 机制自动调用,配合 EnvVarGuard::set 和 EnvVarGuard::remove 防止测试互相污染。
调用图:外部调用 2 个(remove_var, set_var)。
remove_access_token_env_var869–871 ↗
fn remove_access_token_env_var() -> EnvVarGuard
作用:专门临时移除 CODEX_ACCESS_TOKEN_ENV_VAR。这个变量会让系统优先从环境读取访问令牌,所以测试常要清掉它。
数据流:没有业务输入 → 调用 EnvVarGuard::remove 删除访问令牌环境变量 → 返回 guard,测试结束后自动恢复。
调用关系:它被大量 load_auth 和 enforce_login_restrictions 测试调用,用来保证 auth.json 测试不被环境变量抢先。
调用图:被 17 处调用(auth_manager_rejects_stored_personal_access_token_workspace_mismatch, enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_forced_chatgpt_workspace_id_is_set, enforce_login_restrictions_allows_matching_workspace, enforce_login_restrictions_blocks_env_api_key_when_chatgpt_required, enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch, enforce_login_restrictions_logs_out_for_method_mismatch, enforce_login_restrictions_logs_out_for_personal_access_token_workspace_mismatch, enforce_login_restrictions_logs_out_for_workspace_mismatch, loads_api_key_from_auth_json, missing_auth_json_returns_none (+7 more));外部调用 1 个(remove)。
load_auth_reads_access_token_from_env875–923 ↗
async fn load_auth_reads_access_token_from_env()
作用:测试系统能从环境变量里读取 Agent Identity 访问令牌,并且不会把它写进 auth.json。
数据流:生成签名 Agent Identity JWT → mock 公钥接口和任务注册接口 → 设置 CODEX_ACCESS_TOKEN_ENV_VAR 和服务地址 → 调用 load_auth → 得到 AgentIdentity,并确认任务 ID 正确、本地没有写认证文件。
调用关系:它结合 agent_identity_record、signed_agent_identity_jwt、test_jwks_body 和 EnvVarGuard::set,测试 load_auth 的环境变量优先路径。
调用图:调用 4 个内部函数(set, agent_identity_record, signed_agent_identity_jwt, test_jwks_body);外部调用 12 个(given, start, new, assert!, assert_eq!, format!, json!, panic!, load_auth, tempdir (+2 more))。
load_auth_reads_personal_access_token_from_env927–979 ↗
async fn load_auth_reads_personal_access_token_from_env()
作用:测试系统能从环境变量读取个人访问令牌,并通过 whoami 接口补齐账号信息。
数据流:mock whoami 接口返回邮箱、用户 ID、账号 ID、套餐和 FedRAMP 标记 → 设置认证服务地址和访问令牌环境变量 → 分别用文件存储和临时存储加载 → 确认认证信息完整且不写 auth.json。
调用关系:它用 personal_access_token_whoami 提供远端响应,验证 load_auth 对环境个人访问令牌的处理。
调用图:调用 3 个内部函数(set, default, personal_access_token_whoami);外部调用 10 个(given, start, new, assert!, assert_eq!, load_auth, tempdir, header, method, path)。
auth_manager_rejects_env_personal_access_token_workspace_mismatch983–1013 ↗
async fn auth_manager_rejects_env_personal_access_token_workspace_mismatch()
作用:测试 AuthManager 在环境变量个人访问令牌所属工作区不被允许时,会拒绝认证。
数据流:mock whoami 返回被禁止的工作区 → 设置环境变量里的个人访问令牌 → 创建带工作区限制的 AuthManager → 调用 auth 得到 None。
调用关系:它测试 AuthManager::new_with_workspace_restriction 对环境令牌的限制检查。
调用图:调用 4 个内部函数(set, default, personal_access_token_whoami, new_with_workspace_restriction);外部调用 9 个(given, start, new, assert_eq!, tempdir, vec!, header, method, path)。
auth_manager_rejects_stored_personal_access_token_workspace_mismatch1017–1065 ↗
async fn auth_manager_rejects_stored_personal_access_token_workspace_mismatch()
作用:测试已经保存的个人访问令牌,如果后来遇到工作区限制不匹配,AuthManager 也会拒绝使用。
数据流:先用 login_with_access_token 保存一个属于被禁止工作区的个人访问令牌 → 创建带允许工作区列表的 AuthManager → 调用 auth → 得到 None。
调用关系:它先测试保存流程,再测试 AuthManager::new_with_workspace_restriction 的读取限制,覆盖文件和临时存储两种模式。
调用图:调用 5 个内部函数(set, default, personal_access_token_whoami, remove_access_token_env_var, new_with_workspace_restriction);外部调用 10 个(given, start, new, assert_eq!, login_with_access_token, tempdir, vec!, header, method, path)。
personal_access_token_does_not_offer_unauthorized_recovery1069–1104 ↗
async fn personal_access_token_does_not_offer_unauthorized_recovery()
作用:测试个人访问令牌遇到未授权时,不提供自动刷新恢复。因为这种令牌不是 OAuth refresh token 那类可自动续期的凭据。
数据流:设置环境个人访问令牌并让 whoami 成功 → 创建 AuthManager → 请求 unauthorized_recovery → 确认没有下一步,原因是 not_refreshable_auth;调用 refresh_token_from_authority 也不做 OAuth 刷新。
调用关系:它验证 AuthManager 对 PersonalAccessToken 的恢复策略,防止把它当可刷新的 ChatGPT OAuth 登录。
调用图:调用 4 个内部函数(set, default, personal_access_token_whoami, new);外部调用 9 个(new, given, start, new, assert!, assert_eq!, tempdir, method, path)。
load_auth_keeps_codex_api_key_env_precedence1108–1128 ↗
async fn load_auth_keeps_codex_api_key_env_precedence()
作用:测试当 API Key 环境变量和访问令牌环境变量同时存在时,API Key 优先。这样保持旧有优先级规则不变。
数据流:设置 CODEX_ACCESS_TOKEN_ENV_VAR 为假 Agent Identity,同时设置 CODEX_API_KEY_ENV_VAR 为 sk-env → 调用 load_auth 并启用 API Key 环境读取 → 得到 api_key 是 sk-env。
调用关系:它用 fake_agent_identity_jwt 制造另一个可见输入,专门验证 load_auth 的优先级排序。
调用图:调用 3 个内部函数(set, agent_identity_record, fake_agent_identity_jwt);外部调用 3 个(assert_eq!, load_auth, tempdir)。
enforce_login_restrictions_logs_out_for_method_mismatch1132–1158 ↗
async fn enforce_login_restrictions_logs_out_for_method_mismatch()
作用:测试配置要求 ChatGPT 登录时,如果当前是 API Key 登录,系统会报错并登出。
数据流:先保存 API Key 登录 → 构造强制 ChatGPT 的配置 → 调用 enforce_login_restrictions → 得到错误,并确认 auth.json 被删除。
调用关系:它用 build_config 准备限制条件,验证 enforce_login_restrictions 对登录方式不匹配的处理。
调用图:调用 3 个内部函数(default, build_config, remove_access_token_env_var);外部调用 3 个(assert!, enforce_login_restrictions, tempdir)。
enforce_login_restrictions_logs_out_for_workspace_mismatch1162–1193 ↗
async fn enforce_login_restrictions_logs_out_for_workspace_mismatch()
作用:测试 ChatGPT 登录的账号工作区不在允许列表中时,会报错并清除本地认证。
数据流:写入一份账号 ID 为禁止工作区的 ChatGPT auth.json → 构造只允许另一个工作区的配置 → 调用 enforce_login_restrictions → 得到包含允许工作区的错误,并删除 auth.json。
调用关系:它通过 write_auth_file 造不匹配账号,再测试 enforce_login_restrictions 的工作区检查。
调用图:调用 3 个内部函数(build_config, remove_access_token_env_var, write_auth_file);外部调用 4 个(assert!, enforce_login_restrictions, tempdir, vec!)。
enforce_login_restrictions_logs_out_for_personal_access_token_workspace_mismatch1197–1242 ↗
async fn enforce_login_restrictions_logs_out_for_personal_access_token_workspace_mismatch()
作用:测试保存的个人访问令牌属于错误工作区时,登录限制会让它失效并删除认证文件。
数据流:mock whoami 返回被禁止工作区 → 先保存个人访问令牌 → 配置只允许另一个工作区 → 调用 enforce_login_restrictions → 返回错误并删除 auth.json。
调用关系:它把 login_with_access_token 和 enforce_login_restrictions 连起来,检查个人访问令牌也遵守工作区限制。
调用图:调用 4 个内部函数(set, default, personal_access_token_whoami, remove_access_token_env_var);外部调用 10 个(given, start, new, assert!, enforce_login_restrictions, login_with_access_token, tempdir, vec!, method, path)。
enforce_login_restrictions_allows_matching_workspace1246–1273 ↗
async fn enforce_login_restrictions_allows_matching_workspace()
作用:测试当前 ChatGPT 账号属于允许工作区时,登录限制检查会通过,并保留认证文件。
数据流:写入允许工作区的 ChatGPT auth.json → 构造相同允许工作区配置 → 调用 enforce_login_restrictions → 成功返回,auth.json 仍然存在。
调用关系:它是 workspace mismatch 测试的正向版本,确认 enforce_login_restrictions 不会误删正确账号。
调用图:调用 3 个内部函数(build_config, remove_access_token_env_var, write_auth_file);外部调用 4 个(assert!, enforce_login_restrictions, tempdir, vec!)。
enforce_login_restrictions_allows_any_matching_workspace_in_list1277–1302 ↗
async fn enforce_login_restrictions_allows_any_matching_workspace_in_list()
作用:测试允许工作区列表里只要有一个匹配当前账号,就能通过。
数据流:写入账号 ID 为 WORKSPACE_ID_ALLOWED 的 auth.json → 构造包含两个允许工作区的列表,其中一个匹配 → 调用 enforce_login_restrictions → 成功。
调用关系:它使用 build_config 和 write_auth_file,验证 enforce_login_restrictions 对多个允许工作区的判断是“任一匹配即可”。
调用图:调用 2 个内部函数(build_config, write_auth_file);外部调用 3 个(enforce_login_restrictions, tempdir, vec!)。
enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch1306–1366 ↗
async fn enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch()
作用:测试 Agent Identity 属于错误工作区时,也会被登录限制拒绝并清除。
数据流:生成并保存一个属于禁止工作区的签名 Agent Identity → mock 公钥和任务注册接口 → 配置只允许另一个工作区 → 调用 enforce_login_restrictions → 报错并删除 auth.json。
调用关系:它用 signed_agent_identity_jwt、test_jwks_body 和 save_auth 搭出 Agent Identity 登录状态,测试 enforce_login_restrictions 的同一安全规则。
调用图:调用 6 个内部函数(set, default, agent_identity_record, remove_access_token_env_var, signed_agent_identity_jwt, test_jwks_body);外部调用 11 个(given, start, new, assert!, format!, json!, enforce_login_restrictions, tempdir, vec!, method (+1 more))。
enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_forced_chatgpt_workspace_id_is_set1370–1396 ↗
async fn enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_forced_chatgpt_workspace_id_is_set()
作用:测试只配置了允许 ChatGPT 工作区、但没有强制登录方式时,API Key 登录仍然允许。因为 API Key 没有 ChatGPT 工作区可比对。
数据流:保存 API Key 登录 → 构造带工作区限制但不强制 ChatGPT 的配置 → 调用 enforce_login_restrictions → 成功,auth.json 仍存在。
调用关系:它验证 enforce_login_restrictions 的边界条件:工作区限制不应无条件禁止 API Key,除非另有强制登录方式。
调用图:调用 3 个内部函数(default, build_config, remove_access_token_env_var);外部调用 4 个(assert!, enforce_login_restrictions, tempdir, vec!)。
enforce_login_restrictions_blocks_env_api_key_when_chatgpt_required1400–1419 ↗
async fn enforce_login_restrictions_blocks_env_api_key_when_chatgpt_required()
作用:测试环境变量里的 API Key 不能满足“必须 ChatGPT 登录”的要求。
数据流:设置 CODEX_API_KEY_ENV_VAR → 清掉访问令牌环境变量 → 构造强制 ChatGPT 的配置 → 调用 enforce_login_restrictions → 得到说明当前用了 API Key 的错误。
调用关系:它测试 enforce_login_restrictions 不只检查文件里的凭据,也会检查环境变量带来的当前认证方式。
调用图:调用 3 个内部函数(set, build_config, remove_access_token_env_var);外部调用 3 个(assert!, enforce_login_restrictions, tempdir)。
agent_identity_record1421–1433 ↗
fn agent_identity_record(account_id: &str) -> AgentIdentityAuthRecord
作用:生成一份测试用 Agent Identity 记录,里面有代理运行 ID、私钥、账号 ID、用户 ID、邮箱和套餐。
数据流:输入账号 ID → 生成一对代理密钥材料 → 填入固定测试用户信息和传入的账号 ID → 返回 AgentIdentityAuthRecord。
调用关系:它被多条 Agent Identity 测试调用,是生成测试 JWT 前的基础数据来源。
调用图:被 6 处调用(assert_agent_identity_plan_alias, enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch, load_auth_keeps_codex_api_key_env_precedence, load_auth_reads_access_token_from_env, login_with_access_token_rejects_unsigned_jwt, login_with_access_token_writes_only_token);外部调用 1 个(generate_agent_key_material)。
fake_agent_identity_jwt1435–1437 ↗
fn fake_agent_identity_jwt(record: &AgentIdentityAuthRecord) -> std::io::Result<String>
作用:生成一个内容完整但签名不可信的 Agent Identity JWT。它主要用于测试系统会拒绝伪造票据。
数据流:输入 AgentIdentityAuthRecord → 把记录里的 plan_type 转成 JSON 值 → 交给 fake_agent_identity_jwt_with_plan_type → 返回假 JWT。
调用关系:它被无签名拒绝测试和优先级测试使用,内部把实际拼接工作交给 fake_agent_identity_jwt_with_plan_type。
调用图:调用 1 个内部函数(fake_agent_identity_jwt_with_plan_type);被 2 处调用(load_auth_keeps_codex_api_key_env_precedence, login_with_access_token_rejects_unsigned_jwt);外部调用 1 个(to_value)。
fake_agent_identity_jwt_with_plan_type1439–1461 ↗
fn fake_agent_identity_jwt_with_plan_type(
record: &AgentIdentityAuthRecord,
plan_type: serde_json::Value,
) -> std::io::Result<String>
作用:按指定套餐字段拼出一个假的 Agent Identity JWT。这个 JWT 结构像真的,但签名只是普通字符串。
数据流:输入 AgentIdentityAuthRecord 和 plan_type JSON 值 → 构造 JWT header 和 payload → base64 编码后拼成三段字符串 → 返回假 JWT。
调用关系:它由 fake_agent_identity_jwt 调用,用来控制测试里的 plan_type 内容。
调用图:被 1 处调用(fake_agent_identity_jwt);外部调用 3 个(format!, json!, to_vec)。
signed_agent_identity_jwt1463–1486 ↗
fn signed_agent_identity_jwt(
record: &AgentIdentityAuthRecord,
plan_type: serde_json::Value,
) -> jsonwebtoken::errors::Result<String>
作用:生成一个真正用测试 RSA 私钥签过名的 Agent Identity JWT。RSA 是一种公钥私钥签名算法,这里只用于测试。
数据流:输入 AgentIdentityAuthRecord 和 plan_type → 设置 JWT header,kid 为 test-key → 用 TEST_AGENT_IDENTITY_RSA_PRIVATE_KEY_PEM 签名 payload → 返回可被测试公钥验证的 JWT。
调用关系:它被成功登录、环境读取、工作区限制和套餐别名测试调用,配合 test_jwks_body 模拟真实签名验证。
调用图:被 4 处调用(assert_agent_identity_plan_alias, enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch, load_auth_reads_access_token_from_env, login_with_access_token_writes_only_token);外部调用 4 个(json!, from_rsa_pem, new, encode)。
test_jwks_body1488–1499 ↗
fn test_jwks_body() -> serde_json::Value
作用:返回测试用 JWKS 公钥列表。JWKS 是一份 JSON 格式的公钥清单,用来验证 JWT 签名。
数据流:没有输入 → 构造包含 test-key RSA 公钥参数的 JSON → 返回给 mock HTTP 服务器。
调用关系:Agent Identity 相关测试会让 mock 服务器返回它,使被测代码能验证 signed_agent_identity_jwt 生成的令牌。
调用图:被 5 处调用(assert_agent_identity_plan_alias, enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch, load_auth_reads_access_token_from_env, login_with_access_token_rejects_unsigned_jwt, login_with_access_token_writes_only_token);外部调用 1 个(json!)。
personal_access_token_whoami1501–1509 ↗
fn personal_access_token_whoami(account_id: &str) -> serde_json::Value
作用:生成个人访问令牌 whoami 接口的假响应。whoami 可以理解成“这个令牌代表谁”。
数据流:输入账号 ID → 组装邮箱、ChatGPT 用户 ID、账号 ID、business 套餐和 FedRAMP 标记 → 返回 JSON。
调用关系:个人访问令牌登录、环境读取、工作区拒绝和恢复策略测试都会用它作为 mock 服务响应。
调用图:被 7 处调用(auth_manager_rejects_env_personal_access_token_workspace_mismatch, auth_manager_rejects_stored_personal_access_token_workspace_mismatch, enforce_login_restrictions_logs_out_for_personal_access_token_workspace_mismatch, load_auth_reads_personal_access_token_from_env, login_with_access_token_rejects_personal_access_token_workspace_mismatch, login_with_access_token_writes_only_personal_access_token, personal_access_token_does_not_offer_unauthorized_recovery);外部调用 1 个(json!)。
agent_identity_plan_type_maps_raw_enterprise_alias1542–1544 ↗
async fn agent_identity_plan_type_maps_raw_enterprise_alias()
作用:测试 Agent Identity 里的原始套餐值 hc 会被映射成 Enterprise。这里是在确认旧别名或后端别名仍被识别。
数据流:把 JSON 字符串 hc 和期望的 Enterprise 套餐传给 assert_agent_identity_plan_alias → 等待通用断言完成。
调用关系:它本身很薄,把具体搭环境和校验工作交给 assert_agent_identity_plan_alias。
调用图:调用 1 个内部函数(assert_agent_identity_plan_alias);外部调用 1 个(json!)。
agent_identity_plan_type_maps_raw_education_alias1548–1550 ↗
async fn agent_identity_plan_type_maps_raw_education_alias()
作用:测试 Agent Identity 里的原始套餐值 education 会被映射成 Edu。
数据流:把 JSON 字符串 education 和期望的 Edu 套餐传给 assert_agent_identity_plan_alias → 等待通用断言完成。
调用关系:它复用 assert_agent_identity_plan_alias,专门覆盖 education 这个套餐别名。
调用图:调用 1 个内部函数(assert_agent_identity_plan_alias);外部调用 1 个(json!)。
assert_agent_identity_plan_alias1552–1582 ↗
async fn assert_agent_identity_plan_alias(
plan_type: serde_json::Value,
expected_plan_type: AccountPlanType,
)
作用:通用地测试 Agent Identity 的 plan_type 字段能映射到期望套餐。它把生成 JWT、mock 验证服务、读取认证和断言放在一起。
数据流:输入一个 plan_type JSON 值和期望套餐 → 生成 Agent Identity 记录并签名 → mock 公钥和任务注册接口 → 调用 CodexAuth::from_agent_identity_jwt → 检查 account_plan_type 等于期望值。
调用关系:它被两个套餐别名测试调用,内部使用 agent_identity_record、signed_agent_identity_jwt 和 test_jwks_body。
调用图:调用 5 个内部函数(set, agent_identity_record, signed_agent_identity_jwt, test_jwks_body, from_agent_identity_jwt);被 2 处调用(agent_identity_plan_type_maps_raw_education_alias, agent_identity_plan_type_maps_raw_enterprise_alias);外部调用 8 个(given, start, new, format!, json!, assert_eq!, method, path)。
plan_type_maps_known_plan1586–1612 ↗
async fn plan_type_maps_known_plan()
作用:测试 auth.json 里的已知套餐 pro 会映射成对外协议里的 Pro。
数据流:写入 chatgpt_plan_type 为 pro 的 auth.json → 调用 load_auth → 读取 account_plan_type → 确认是 AccountPlanType::Pro。
调用关系:它用 write_auth_file 造 ChatGPT token 数据,验证 load_auth 之后的套餐转换。
调用图:调用 2 个内部函数(remove_access_token_env_var, write_auth_file);外部调用 3 个(assert_eq!, load_auth, tempdir)。
plan_type_maps_self_serve_business_usage_based_plan1616–1645 ↗
async fn plan_type_maps_self_serve_business_usage_based_plan()
作用:测试 self_serve_business_usage_based 这个套餐字符串会被正确映射。
数据流:写入带 self_serve_business_usage_based 的 auth.json → 调用 load_auth → 检查 account_plan_type 是 SelfServeBusinessUsageBased。
调用关系:它属于一组套餐映射测试,确保不同后端套餐名不会在客户端丢失含义。
调用图:调用 2 个内部函数(remove_access_token_env_var, write_auth_file);外部调用 3 个(assert_eq!, load_auth, tempdir)。
plan_type_maps_enterprise_cbp_usage_based_plan1649–1678 ↗
async fn plan_type_maps_enterprise_cbp_usage_based_plan()
作用:测试 enterprise_cbp_usage_based 这个企业用量套餐字符串会被正确映射。
数据流:写入带 enterprise_cbp_usage_based 的 auth.json → 调用 load_auth → 检查 account_plan_type 是 EnterpriseCbpUsageBased。
调用关系:它和其他 plan_type_maps 测试一起覆盖套餐字段从内部格式到协议格式的转换。
调用图:调用 2 个内部函数(remove_access_token_env_var, write_auth_file);外部调用 3 个(assert_eq!, load_auth, tempdir)。
plan_type_maps_unknown_to_unknown1682–1708 ↗
async fn plan_type_maps_unknown_to_unknown()
作用:测试遇到不认识的套餐字符串时,不会崩溃,而是映射成 Unknown。
数据流:写入 chatgpt_plan_type 为 mystery-tier 的 auth.json → 调用 load_auth → 检查 account_plan_type 是 Unknown。
调用关系:它验证 load_auth 和套餐转换逻辑的兼容性,防止后端新增套餐时客户端直接失败。
调用图:调用 2 个内部函数(remove_access_token_env_var, write_auth_file);外部调用 3 个(assert_eq!, load_auth, tempdir)。
missing_plan_type_maps_to_unknown1712–1738 ↗
async fn missing_plan_type_maps_to_unknown()
作用:测试 auth.json 里完全没有套餐字段时,套餐结果是 Unknown。
数据流:写入没有 chatgpt_plan_type 的 auth.json → 调用 load_auth → 检查 account_plan_type 是 Unknown。
调用关系:它覆盖套餐字段缺失的情况,和未知套餐字符串测试一起保证解析逻辑稳健。
调用图:调用 2 个内部函数(remove_access_token_env_var, write_auth_file);外部调用 3 个(assert_eq!, load_auth, tempdir)。
login/src/auth/bedrock_api_key_tests.rs源码 ↗
这个文件是一组自动化测试。它把临时文件夹当成一个假的 Codex 用户目录,在里面写入和读取登录信息,检查 Bedrock API Key 这种登录方式是否表现正确。Bedrock 可以理解成另一套云服务入口,它需要自己的 API Key 和地区 region。测试会覆盖几个关键场景:用 Bedrock 登录时要替换掉旧的 OpenAI API Key;退出登录时要把 Bedrock 凭据删干净;如果磁盘里只有 Bedrock 凭据,启动认证管理器时也要能把它识别成当前登录方式;反过来,如果之后用普通 API Key 登录,也必须清掉 Bedrock 信息。文件里的几个小函数只是造测试数据,像准备不同口味的“假登录档案”。真正被检查的是 AuthManager、FileAuthStorage 和登录/退出函数能不能一起把认证状态维护一致。
api_key_auth14–24 ↗
fn api_key_auth() -> AuthDotJson
作用:这个函数造出一份“只使用 OpenAI API Key 登录”的假认证文件内容。测试用它来模拟用户原本已经用普通 API Key 登录过。
数据流:进去没有参数 → 它在内存里拼出一个 AuthDotJson,里面有 auth_mode 为 ApiKey 和一条 sk-test-key,其他登录方式都留空 → 出来一份可保存、可比较的测试认证数据,不会改动磁盘。
调用关系:它被 login_with_bedrock_api_key_replaces_openai_auth 用来先写入旧的 API Key 登录状态,也在后续比较里作为“应该长什么样”的参考数据之一。它自己不调用别的项目函数,只是准备测试样本。
调用图:被 1 处调用(login_with_bedrock_api_key_replaces_openai_auth)。
bedrock_only_auth26–36 ↗
fn bedrock_only_auth() -> AuthDotJson
作用:这个函数造出一份“只有 Bedrock API Key 信息、但没有明确 auth_mode”的假认证文件内容。它用来测试系统能不能从已有字段里推断出当前应该是 Bedrock 登录。
数据流:进去没有参数 → 它调用 bedrock_auth 生成 Bedrock 的 key 和地区,再放进 AuthDotJson,同时让 auth_mode 保持为空 → 出来一份只有 Bedrock 凭据的测试认证数据。
调用关系:它被 bedrock_only_auth_storage_creates_primary_auth 调用,负责准备一个有点特殊的旧格式或不完整状态。它把具体的 Bedrock 凭据构造工作交给 bedrock_auth,这样多个测试拿到的是同一套固定数据。
调用图:调用 1 个内部函数(bedrock_auth);被 1 处调用(bedrock_only_auth_storage_creates_primary_auth)。
bedrock_auth38–43 ↗
fn bedrock_auth() -> BedrockApiKeyAuth
作用:这个函数造出一份固定的 Bedrock 登录凭据,包括 API Key 和区域。它让多个测试都用同一份标准答案,避免到处重复写字符串。
数据流:进去没有参数 → 它创建一个 BedrockApiKeyAuth,填入 bedrock-api-key-test 和 us-east-1 → 出来一份内存里的 Bedrock 凭据对象,不读取也不写入外部文件。
调用关系:它被 bedrock_only_auth 用来填充认证文件,也被 login_with_bedrock_api_key_replaces_openai_auth 用来和认证管理器读出的结果做比较。它是这些测试里的“标准 Bedrock 凭据模板”。
调用图:被 2 处调用(bedrock_only_auth, login_with_bedrock_api_key_replaces_openai_auth)。
login_with_bedrock_api_key_replaces_openai_auth47–92 ↗
async fn login_with_bedrock_api_key_replaces_openai_auth() -> anyhow::Result<()>
作用:这个测试确认:如果用户原来是 OpenAI API Key 登录,后来改用 Bedrock API Key 登录,旧的 OpenAI Key 必须被清掉,当前登录方式也要变成 Bedrock。
数据流:进去没有外部输入,测试自己创建临时目录 → 先用 api_key_auth 写入一份旧的 OpenAI API Key 登录文件,再调用 Bedrock 登录函数,然后创建 AuthManager 重新读取认证状态 → 出来是多条断言:磁盘里的认证文件只剩 Bedrock 信息,认证管理器缓存里也只认 Bedrock 登录。
调用关系:这是一个端到端性质的小测试:它用 tempdir 准备隔离环境,用 FileAuthStorage::new 操作测试认证文件,用 login_with_bedrock_api_key 执行真正登录,再用 AuthManager::new 模拟程序启动后读取状态。最后它用 bedrock_auth 作为期望值,检查保存层和管理层是否一致。
调用图:调用 5 个内部函数(default, api_key_auth, bedrock_auth, new, new);外部调用 2 个(assert_eq!, tempdir)。
logout_removes_bedrock_auth96–120 ↗
async fn logout_removes_bedrock_auth() -> anyhow::Result<()>
作用:这个测试确认:用户用 Bedrock API Key 登录后,再退出登录,磁盘和内存里的认证信息都应该被清空。
数据流:进去没有外部输入,测试创建一个临时用户目录 → 先调用 Bedrock 登录函数写入凭据,再创建 AuthManager 读取它,然后执行 logout → 出来是断言结果:logout 返回成功,认证文件不存在或为空,AuthManager 的缓存也变成空。
调用关系:它模拟真实用户“登录再退出”的流程。Bedrock 登录函数先留下数据,AuthManager::new 接手读取数据,auth_manager.logout 再负责清理。测试最后通过 FileAuthStorage 和 AuthManager 两边确认清理没有只做一半。
调用图:调用 3 个内部函数(default, new, new);外部调用 3 个(assert!, assert_eq!, tempdir)。
bedrock_only_auth_storage_creates_primary_auth124–151 ↗
async fn bedrock_only_auth_storage_creates_primary_auth() -> anyhow::Result<()>
作用:这个测试确认:即使认证文件里没有明确写 auth_mode,只要里面有 Bedrock API Key,系统启动时也应该把它当成 Bedrock 登录。
数据流:进去没有外部输入,测试创建临时目录 → 用 bedrock_only_auth 写入一份只有 Bedrock 字段的认证文件,再创建 AuthManager 读取 → 出来是断言:认证模式被识别为 BedrockApiKey,缓存中的认证对象也等于 bedrock_auth 生成的标准凭据。
调用关系:它主要检查 AuthManager::new 的读取和兼容能力。bedrock_only_auth 负责制造特殊输入,FileAuthStorage::new 负责把它放到临时目录,AuthManager::new 负责把文件变成程序内部的当前登录状态。
调用图:调用 4 个内部函数(default, bedrock_only_auth, new, new);外部调用 2 个(assert_eq!, tempdir)。
login_with_api_key_clears_bedrock_api_key154–174 ↗
async fn login_with_api_key_clears_bedrock_api_key() -> anyhow::Result<()>
作用:这个测试确认:如果用户先用 Bedrock API Key 登录,后来改用普通 API Key 登录,旧的 Bedrock 凭据必须被清掉。
数据流:进去没有外部输入,测试创建临时目录 → 先写入 Bedrock 登录信息,再调用 login_with_api_key 写入普通 API Key → 出来是断言:认证文件最终等于 api_key_auth,也就是只保留普通 API Key,不再含 Bedrock 字段。
调用关系:它检查另一条切换方向,和 login_with_bedrock_api_key_replaces_openai_auth 互为补充。前者看 Bedrock 是否能替换普通 API Key,这里看普通 API Key 是否能替换 Bedrock。它把实际切换动作交给 crate::auth::login_with_api_key,最后用 FileAuthStorage 读取结果来验收。
调用图:调用 2 个内部函数(default, new);外部调用 3 个(assert_eq!, login_with_api_key, tempdir)。
登录流程集成
这些集成测试覆盖主要的用户登录入口,从 CLI 调用到设备代码和基于浏览器的服务器流程。
cli/tests/login.rs源码 ↗
登录功能一旦出错,用户可能会以为自己登录成功了,实际却没有可用凭证;或者旧的刷新令牌还留着,带来安全和账号混乱的问题。这个测试文件就像给登录流程做“验收检查”。它会创建一个临时的 CODEX_HOME,也就是临时的用户配置目录,避免污染真实电脑上的文件。然后它运行真正的 codex 命令,模拟用户从键盘输入 API key 或访问令牌,并检查命令是否成功、错误信息是否合理、auth.json 里到底写了什么。设备登录那部分还启动了一个假的 OAuth 服务器(OAuth 是一种常见网页登录授权流程),用来假装远端登录服务,记录 Codex 发来的请求。这样测试就能确认:如果本地已经有旧 token,Codex 会先调用撤销接口,再按顺序拿用户码、授权码和新 token。
codex_command18–22 ↗
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command>
作用:这个辅助函数用来准备一条可以运行 codex 程序的测试命令。它还会把 CODEX_HOME 指到临时目录,让测试用自己的“假家目录”,不碰用户真实配置。
数据流:进去的是一个临时目录路径 → 它找到测试用的 codex 可执行文件,创建命令对象,并给这个命令设置 CODEX_HOME 环境变量 → 出来的是一条已经准备好、后续可以继续加参数并运行的命令。
调用关系:三个登录测试都会先调用它来启动真正的 codex 命令。它自己把“找到程序”和“设置测试目录”这两件基础活做好,后面的测试只需要关心传什么登录参数、输入什么内容、检查什么结果。
调用图:被 3 处调用(device_login_revokes_existing_auth_before_requesting_new_tokens, login_with_access_token_rejects_invalid_jwt, login_with_api_key_reads_stdin_and_writes_auth_json);外部调用 2 个(new, cargo_bin)。
write_file_auth_config24–30 ↗
fn write_file_auth_config(codex_home: &Path) -> Result<()>
作用:这个辅助函数给临时配置目录写入一份配置,明确告诉 Codex:登录凭证要保存到文件里。这样测试才能稳定地去读 auth.json,而不是让系统钥匙串之类的外部工具介入。
数据流:进去的是临时配置目录路径 → 它在里面创建或覆盖 config.toml,并写入 cli_auth_credentials_store = "file" → 出来没有业务数据,但磁盘上多了一份让登录凭证走文件存储的配置。
调用关系:三个测试在运行登录命令前都会调用它。它相当于先把测试环境的规则定好,确保后面的 codex_command 运行时会把结果写到这个临时目录里的 auth.json,方便 read_auth_json 再读取检查。
调用图:被 3 处调用(device_login_revokes_existing_auth_before_requesting_new_tokens, login_with_access_token_rejects_invalid_jwt, login_with_api_key_reads_stdin_and_writes_auth_json);外部调用 2 个(join, write)。
read_auth_json32–35 ↗
fn read_auth_json(codex_home: &Path) -> Result<Value>
作用:这个辅助函数读取登录后生成的 auth.json,并把它解析成方便检查的 JSON 数据。测试用它来确认 Codex 到底保存了什么凭证。
数据流:进去的是临时配置目录路径 → 它读取该目录下的 auth.json 文本,再把 JSON 字符串解析成 serde_json::Value 这种通用 JSON 值 → 出来的是可以用下标检查字段的登录信息数据。
调用关系:API key 登录测试和设备登录测试会在命令执行成功后调用它。它承接登录命令写出的文件,把磁盘上的结果变成测试断言可以直接比较的数据。
调用图:被 2 处调用(device_login_revokes_existing_auth_before_requesting_new_tokens, login_with_api_key_reads_stdin_and_writes_auth_json);外部调用 3 个(join, from_str, read_to_string)。
login_with_api_key_reads_stdin_and_writes_auth_json38–60 ↗
fn login_with_api_key_reads_stdin_and_writes_auth_json() -> Result<()>
作用:这个测试确认:用户用 --with-api-key 登录时,Codex 会从标准输入读取 API key,并把它正确写进 auth.json。标准输入可以理解成用户在命令行里敲进去的内容。
数据流:开始时创建一个干净的临时目录,并写入“凭证存文件”的配置 → 它运行 codex login --with-api-key,把 sk-test 加换行作为输入喂给命令 → 命令成功后,测试读取 auth.json,确认 OPENAI_API_KEY 是 sk-test,同时确认没有多余的 tokens 或 agent_identity 字段。
调用关系:这个测试使用 write_file_auth_config 搭好配置,用 codex_command 启动真正的命令,再用 read_auth_json 检查结果。它覆盖的是最直接的 API key 登录路径,确保用户输入能落到正确文件里。
调用图:调用 3 个内部函数(codex_command, read_auth_json, write_file_auth_config);外部调用 4 个(new, assert!, assert_eq!, contains)。
login_with_access_token_rejects_invalid_jwt63–75 ↗
fn login_with_access_token_rejects_invalid_jwt() -> Result<()>
作用:这个测试确认:用户用访问令牌登录时,如果输入的不是合法 JWT,Codex 会拒绝它并报错。JWT 是一种带固定格式的令牌,通常长得像三段用点号隔开的字符串。
数据流:开始时创建临时目录并写好文件存储配置 → 它运行 codex login --with-access-token,并输入 not-a-jwt → 结果应该是命令失败,错误输出里包含“Error logging in with access token”。
调用关系:这个测试同样先用 write_file_auth_config 和 codex_command 搭建环境,但它不读取 auth.json,因为重点是坏输入必须被挡住。它守住的是登录入口的校验关,防止无效令牌被当成成功登录保存下来。
调用图:调用 2 个内部函数(codex_command, write_file_auth_config);外部调用 2 个(new, contains)。
device_login_revokes_existing_auth_before_requesting_new_tokens78–176 ↗
async fn device_login_revokes_existing_auth_before_requesting_new_tokens() -> Result<()>
作用:这个测试确认设备登录的安全顺序:如果本地已有旧登录信息,Codex 会先撤销旧的刷新令牌,再去申请新的登录 token。刷新令牌可以用来换取新访问权限,所以旧的不能随便留着。
数据流:它先启动一个假的登录服务器,并安排四个接口按预期返回:撤销旧 token、获取用户码、获取授权信息、换取新 token → 然后在临时 auth.json 里预放旧的 refresh_token → 接着运行 codex login --device-auth,让 Codex 访问这个假服务器 → 最后它检查服务器收到的请求顺序必须是先撤销再登录,并确认撤销请求里带的是 old-refresh,auth.json 里保存的新 refresh_token 变成 new-refresh。
调用关系:这是本文件最完整的流程测试。它用 wiremock 伪造远端 OAuth 服务,用 write_file_auth_config 和 codex_command 启动真实命令,用 read_auth_json 检查最终文件。它把“旧凭证清理”和“新凭证获取”串起来验证,确保设备登录不是只看最后成功,而是全过程顺序也正确。
调用图:调用 3 个内部函数(codex_command, read_auth_json, write_file_auth_config);外部调用 12 个(given, start, new, new, assert_eq!, format!, json!, contains, to_vec, write (+2 more))。
login/tests/suite/device_code_login.rs源码 ↗
设备码登录可以理解成:程序先向服务器要一个临时验证码,让用户去网页确认;程序再不停询问服务器“用户确认了吗”;确认后换取正式登录令牌。这个测试文件用 WireMock(一个假的 HTTP 服务器,用来假装远端服务)模拟这些接口。它会创建临时目录当作用户的 Codex 配置目录,避免污染真实电脑;再安排假服务器返回成功、失败、工作区不匹配等不同情况;最后运行真正的 run_device_code_login,并检查 auth.json 有没有被正确写入。重要的是,这里不只测“成功”,也测“失败时不能留下半截登录信息”。比如验证码接口报错、用户拒绝授权、账号工作区不允许时,都应该报错并且不保存凭据。文件里的小辅助函数就像搭舞台的道具:造 JWT、布置假接口、生成登录参数;测试函数则像一场场彩排,确认登录这台机器在各种情况都不会出岔子。
make_jwt30–36 ↗
fn make_jwt(payload: serde_json::Value) -> String
作用:造一个测试用的 JWT 字符串。JWT 可以简单理解成一张带着用户信息的“电子票据”,这里不追求真实安全签名,只为了让登录代码能读到里面的账号信息。
数据流:输入是一段 JSON 形式的票据内容 → 函数给它配上固定的 JWT 头,把头和内容都转成字节,再用 URL 安全的 Base64 编码拼起来,最后加上一个假的签名 → 输出一个形如“头.内容.签名”的字符串,供后面的假登录接口返回。
调用关系:成功登录测试、工作区不匹配测试、没有 API key 也能保存令牌的测试都会用它先准备 id_token。它本身只造数据,不访问网络;造好的 JWT 会交给 mock_oauth_token_single,让假 OAuth 接口返回给真正的登录流程。
调用图:被 3 处调用(device_code_login_integration_persists_without_api_key_on_exchange_failure, device_code_login_integration_succeeds, device_code_login_rejects_workspace_mismatch);外部调用 3 个(format!, json!, to_vec)。
mock_usercode_success38–49 ↗
async fn mock_usercode_success(server: &MockServer)
作用:给假的服务器装上“申请设备验证码成功”的接口。也就是说,当登录流程来要验证码时,服务器会返回设备认证编号、用户要输入的代码,以及轮询间隔。
数据流:输入是假服务器对象 → 函数在这个服务器上注册一个 POST /api/accounts/deviceauth/usercode 规则,返回 200 和一段固定 JSON → 之后登录流程请求这个地址时,就会得到 CODE-12345 这类测试验证码。
调用关系:大多数测试一开始都会调用它,因为设备码登录的第一步就是拿用户验证码。它把第一关布置成成功,然后测试函数再继续布置轮询接口和换令牌接口。
调用图:被 4 处调用(device_code_login_integration_handles_error_payload, device_code_login_integration_persists_without_api_key_on_exchange_failure, device_code_login_integration_succeeds, device_code_login_rejects_workspace_mismatch);外部调用 5 个(given, new, json!, method, path)。
mock_usercode_failure51–57 ↗
async fn mock_usercode_failure(server: &MockServer, status: u16)
作用:给假的服务器装上“申请设备验证码失败”的接口。它用来验证:如果登录第一步就被服务器拒绝,程序应该把错误抛出来,而不是继续假装登录成功。
数据流:输入是假服务器对象和想要返回的 HTTP 状态码,比如 503 → 函数注册 POST /api/accounts/deviceauth/usercode 规则,但只返回这个失败状态,不返回成功内容 → 登录流程一请求验证码,就会收到失败并中断。
调用关系:只有 device_code_login_integration_handles_usercode_http_failure 使用它。这个测试专门把第一步设置成失败,然后检查 run_device_code_login 会报错,并且不会写 auth.json。
调用图:被 1 处调用(device_code_login_integration_handles_usercode_http_failure);外部调用 4 个(given, new, method, path)。
mock_poll_token_two_step59–82 ↗
async fn mock_poll_token_two_step(
server: &MockServer,
counter: Arc<AtomicUsize>,
first_response_status: u16,
)
作用:模拟“程序第一次问还没好,第二次问就拿到授权码”的轮询过程。轮询就是程序隔一会儿问服务器一次:用户是否已经在网页上确认了。
数据流:输入是假服务器、一个计数器、第一次请求要返回的状态码 → 函数给 POST /api/accounts/deviceauth/token 注册一个会记次数的响应:第一次返回指定失败状态,第二次返回授权码、code challenge 和 code verifier → 输出不是直接返回数据,而是改变假服务器行为,让真实登录流程经历两步等待后成功。
调用关系:成功登录、工作区不匹配、没有 API key 的测试都会用它。它接在 mock_usercode_success 后面,扮演设备码登录的第二阶段;之后登录流程会拿它返回的授权码去调用 OAuth 换令牌接口。
调用图:被 3 处调用(device_code_login_integration_persists_without_api_key_on_exchange_failure, device_code_login_integration_succeeds, device_code_login_rejects_workspace_mismatch);外部调用 3 个(given, method, path)。
mock_poll_token_single84–90 ↗
async fn mock_poll_token_single(server: &MockServer, endpoint: &str, response: ResponseTemplate)
作用:给假的服务器装一个“一次性轮询响应”。它更灵活,可以让某个接口直接返回指定结果,比如错误 JSON。
数据流:输入是假服务器、接口路径和一份预先准备好的响应模板 → 函数注册一个 POST 到该路径的规则,并让服务器按模板返回 → 后续登录流程访问这个路径时,就会收到测试指定的成功或失败内容。
调用关系:device_code_login_integration_handles_error_payload 用它模拟设备授权接口返回错误 payload。它不像 mock_poll_token_two_step 那样控制两次请求,而是专门服务于错误分支测试。
调用图:被 1 处调用(device_code_login_integration_handles_error_payload);外部调用 3 个(given, method, path)。
mock_oauth_token_single92–102 ↗
async fn mock_oauth_token_single(server: &MockServer, jwt: String)
作用:模拟 OAuth 换令牌接口成功返回。OAuth 可以理解成一种“拿临时授权码换正式登录凭证”的通用登录协议。
数据流:输入是假服务器和一个 JWT 字符串 → 函数注册 POST /oauth/token,返回 200,并带上 id_token、access_token、refresh_token → 登录流程拿到这些值后,会把它们当作正式登录令牌保存起来。
调用关系:它通常放在 mock_poll_token_two_step 后面使用,因为只有轮询拿到授权码后,登录流程才会来换令牌。成功登录、工作区不匹配、无 API key 场景都通过它提供最终令牌。
调用图:被 3 处调用(device_code_login_integration_persists_without_api_key_on_exchange_failure, device_code_login_integration_succeeds, device_code_login_rejects_workspace_mismatch);外部调用 5 个(given, new, json!, method, path)。
server_opts104–119 ↗
fn server_opts(
codex_home: &tempfile::TempDir,
issuer: String,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> ServerOptions
作用:快速生成一份测试用的登录服务器配置。它把临时目录、客户端 ID、凭据保存方式、假服务器地址这些信息装进 ServerOptions。
数据流:输入是临时 Codex 主目录、issuer 地址和凭据保存模式 → 函数创建 ServerOptions,设置 issuer 指向假服务器,并关闭自动打开浏览器 → 输出一份可以直接交给 run_device_code_login 的配置。
调用关系:多个测试用它减少重复代码。测试先启动假服务器,再用 server_opts 把登录流程指向这台假服务器,这样 run_device_code_login 会访问测试布置好的接口,而不是访问真实线上服务。
调用图:调用 2 个内部函数(default, new);被 3 处调用(device_code_login_integration_handles_usercode_http_failure, device_code_login_integration_succeeds, device_code_login_rejects_workspace_mismatch);外部调用 1 个(path)。
device_code_login_integration_succeeds122–166 ↗
async fn device_code_login_integration_succeeds() -> anyhow::Result<()>
作用:验证最标准的设备码登录成功路径:能拿验证码、轮询成功、换到令牌,并把登录信息写进 auth.json。
数据流:开始时创建一个空的临时 Codex 目录和假服务器 → 布置验证码成功、轮询两步成功、OAuth 返回令牌,并造一个含允许工作区账号的 JWT → 运行 run_device_code_login → 最后读取 auth.json,确认 access token、refresh token、id token 和账号 ID 都保存正确。
调用关系:这是整组测试的主线。它把 make_jwt、mock_usercode_success、mock_poll_token_two_step、mock_oauth_token_single 和 server_opts 串起来,然后调用真正的 run_device_code_login,最后用 load_auth_dot_json 检查结果。
调用图:调用 6 个内部函数(default, make_jwt, mock_oauth_token_single, mock_poll_token_two_step, mock_usercode_success, server_opts);外部调用 9 个(new, new, start, assert_eq!, load_auth_dot_json, run_device_code_login, json!, skip_if_no_network!, tempdir)。
device_code_login_rejects_workspace_mismatch169–213 ↗
async fn device_code_login_rejects_workspace_mismatch() -> anyhow::Result<()>
作用:验证登录账号所属工作区不在允许列表里时,程序必须拒绝登录。这样可以防止用户把不该用的账号凭据保存进本地配置。
数据流:测试先创建临时目录和假服务器 → 假服务器返回一个 JWT,里面写的是不被允许的工作区 ID → 配置里强制要求只能使用另一个允许的工作区 → 运行登录后应得到 PermissionDenied 错误 → 再读取 auth.json,确认文件没有被创建。
调用关系:它复用成功路径里的假验证码、两步轮询和 OAuth 返回,但故意让 make_jwt 生成不匹配的账号信息。run_device_code_login 在保存前应做工作区校验,load_auth_dot_json 用来确认失败后没有留下凭据。
调用图:调用 6 个内部函数(default, make_jwt, mock_oauth_token_single, mock_poll_token_two_step, mock_usercode_success, server_opts);外部调用 11 个(new, new, start, assert!, assert_eq!, load_auth_dot_json, run_device_code_login, json!, skip_if_no_network!, tempdir (+1 more))。
device_code_login_integration_handles_usercode_http_failure216–248 ↗
async fn device_code_login_integration_handles_usercode_http_failure() -> anyhow::Result<()>
作用:验证申请设备验证码这一步如果 HTTP 请求失败,错误会被正常传出来。HTTP 可以简单理解成程序和服务器说话的网络请求格式。
数据流:测试创建临时目录和假服务器 → 用 mock_usercode_failure 让验证码接口返回 503 这类失败状态 → 运行登录流程 → 得到错误,并检查错误文字提到设备码请求失败 → 最后确认 auth.json 仍然不存在。
调用关系:这是第一步失败的测试。它只需要布置 usercode 失败,不再布置后面的轮询和 OAuth,因为 run_device_code_login 应该在第一关就停下;load_auth_dot_json 负责验证没有错误写入。
调用图:调用 3 个内部函数(default, mock_usercode_failure, server_opts);外部调用 6 个(start, assert!, load_auth_dot_json, run_device_code_login, skip_if_no_network!, tempdir)。
device_code_login_integration_persists_without_api_key_on_exchange_failure251–301 ↗
async fn device_code_login_integration_persists_without_api_key_on_exchange_failure() -> anyhow::Result<()>
作用:验证即使没有拿到 OpenAI API key,设备登录拿到的令牌仍然会被保存。也就是说,API key 交换失败不应该抹掉已经成功获得的登录令牌。
数据流:测试创建临时目录和假服务器 → 布置验证码成功、轮询成功、OAuth 返回令牌,但 JWT 内容为空,不带账号相关信息 → 运行登录流程 → 读取 auth.json,确认 openai_api_key 是空的,但 access token、refresh token 和 id token 都被保存。
调用关系:它走的是“登录本身成功,但附加信息不完整”的路径。它调用 make_jwt 造空 payload,通过 mock_oauth_token_single 返回,再让 run_device_code_login 保存结果,最后用 load_auth_dot_json 检查保存内容。
调用图:调用 6 个内部函数(default, new, make_jwt, mock_oauth_token_single, mock_poll_token_two_step, mock_usercode_success);外部调用 10 个(new, new, start, assert!, assert_eq!, load_auth_dot_json, run_device_code_login, json!, skip_if_no_network!, tempdir)。
device_code_login_integration_handles_error_payload304–360 ↗
async fn device_code_login_integration_handles_error_payload() -> anyhow::Result<()>
作用:验证设备授权接口返回明确错误内容时,登录流程会失败,并且不会写入本地登录文件。比如服务器说 authorization_declined,就表示用户拒绝或授权没通过。
数据流:测试创建临时目录和假服务器 → 先让验证码接口成功 → 再让轮询接口直接返回 401 和错误 JSON,里面有 authorization_declined → 运行登录流程并期待报错 → 检查错误信息包含拒绝授权或状态码 → 最后确认 auth.json 没有生成。
调用关系:它用 mock_usercode_success 让第一步过关,再用 mock_poll_token_single 精准制造第二步的错误。run_device_code_login 应把这个错误向上传递;load_auth_dot_json 则检查失败时没有保存任何凭据。
调用图:调用 4 个内部函数(default, new, mock_poll_token_single, mock_usercode_success);外部调用 8 个(start, new, assert!, load_auth_dot_json, run_device_code_login, json!, skip_if_no_network!, tempdir)。
login/tests/suite/login_server_e2e.rs源码 ↗
这个测试文件像一次登录彩排:先搭一个假的发证服务器,也就是冒充 OAuth 服务端的本地小网站;再启动 Codex 的本地登录服务器;然后用 HTTP 客户端假装浏览器访问回调地址。OAuth 可以理解成“网站让第三方确认你是谁”的登录流程。测试重点不是某个小函数,而是整条链路有没有跑通:旧的 auth.json 会不会被新令牌覆盖,缺失的配置目录会不会自动创建,限制工作区时会不会把不允许的账号挡住,用户拒绝或没权限时会不会显示明白的错误页。它还测试端口冲突:默认端口被占用时要换备用端口;如果已有旧登录服务器占着端口,新服务器要能把旧的取消掉。没有这些测试,登录这种牵涉浏览器、本地端口、文件写入和错误提示的流程,很容易在真实用户机器上才暴雷。
start_mock_issuer28–89 ↗
fn start_mock_issuer(chatgpt_account_id: &str) -> (SocketAddr, thread::JoinHandle<()>)
作用:启动一个假的登录发证服务器,供测试里的登录服务器交换令牌用。它让测试不用真的连到外部 OpenAI 登录系统,也能拿到一组看起来像真的登录令牌。
数据流:进去的是一个 ChatGPT 账号或工作区 ID。函数先在本机随机找一个可用端口,开一个很小的 HTTP 服务器;当收到 /oauth/token 请求时,它生成一份假的 JWT,也就是一段带用户信息的登录票据,再连同 access_token 和 refresh_token 一起返回。出来的是服务器地址和后台线程句柄;同时它在后台持续监听请求。
调用关系:所有端到端测试都会先用它搭好“假登录服务”。后面的测试再调用 run_login_server 启动真正要测的 Codex 登录服务器,让 Codex 去找这个假服务换令牌,从而验证本地登录逻辑,而不是依赖真实网络服务。
调用图:被 8 处调用(cancels_previous_login_server_when_port_is_in_use, creates_missing_codex_home_dir, end_to_end_login_flow_persists_auth_json, falls_back_to_registered_fallback_port_when_default_port_is_in_use, forced_chatgpt_workspace_id_mismatch_blocks_login, login_server_includes_forced_workspaces_as_one_query_param, oauth_access_denied_missing_entitlement_blocks_login_with_clear_error, oauth_access_denied_unknown_reason_uses_generic_error_page);外部调用 3 个(bind, spawn, from_listener)。
end_to_end_login_flow_persists_auth_json92–169 ↗
async fn end_to_end_login_flow_persists_auth_json() -> Result<()>
作用:验证一次完整登录成功后,新的认证信息会正确写进 auth.json,并覆盖旧的脏数据。auth.json 可以理解成 Codex 存登录凭证的小本子。
数据流:测试先准备临时目录,并故意写入一份过期的 auth.json。然后启动假发证服务器和本地登录服务器,用 HTTP 请求假装浏览器带着 code 和 state 回来。服务器拿 code 换到假令牌后结束;测试再读取 auth.json,确认 API key、access_token、refresh_token 和 account_id 都变成了新值。
调用关系:它先调用 start_mock_issuer 准备令牌来源,再调用 run_login_server 跑被测服务,最后通过 reqwest 这个 HTTP 客户端模拟浏览器回调。它是本文件最完整的成功路径测试,证明登录链路最终真的落到了磁盘文件上。
调用图:调用 1 个内部函数(start_mock_issuer);外部调用 14 个(assert!, assert_eq!, builder, run_login_server, limited, format!, from_str, json!, to_string_pretty, skip_if_no_network! (+4 more))。
creates_missing_codex_home_dir172–213 ↗
async fn creates_missing_codex_home_dir() -> Result<()>
作用:验证如果 Codex 的配置目录一开始不存在,登录成功时也能自动创建目录和 auth.json。这样用户第一次使用时不需要手动建文件夹。
数据流:进去的测试条件是一个还不存在的临时子目录。测试启动假发证服务器和本地登录服务器,然后模拟浏览器回调。登录完成后,它检查那个原本不存在的目录下面是否出现了 auth.json。
调用关系:它复用 start_mock_issuer 提供假令牌,再让 run_login_server 走正常登录流程。和完整成功测试相比,它专门盯住“父目录缺失”这个容易被忽略的文件系统场景。
调用图:调用 2 个内部函数(new, start_mock_issuer);外部调用 5 个(assert!, run_login_server, format!, skip_if_no_network!, tempdir)。
login_server_includes_forced_workspaces_as_one_query_param216–255 ↗
async fn login_server_includes_forced_workspaces_as_one_query_param() -> Result<()>
作用:验证当登录被限制到多个指定工作区时,登录链接里会把这些工作区 ID 合成一个 allowed_workspace_id 参数。这样浏览器跳转到登录页时,远端能知道只允许哪些工作区。
数据流:测试输入两个允许的工作区 ID,然后启动登录服务器但不真的完成登录。它解析服务器生成的 auth_url,查看查询参数,也就是网址问号后面的那些键值。结果应该只有一个 allowed_workspace_id,值是两个 ID 用逗号连起来。
调用关系:它调用 start_mock_issuer 只是为了让登录服务器配置完整,核心动作是调用 run_login_server 并检查返回的 auth_url。它位于登录开始阶段,确保用户还没点登录前,限制条件已经写进跳转链接。
调用图:调用 1 个内部函数(start_mock_issuer);外部调用 7 个(parse, assert_eq!, run_login_server, format!, skip_if_no_network!, tempdir, vec!)。
forced_chatgpt_workspace_id_mismatch_blocks_login258–316 ↗
async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()>
作用:验证如果用户登录回来的账号不属于允许的工作区,登录会被拒绝,而且不会写入 auth.json。这个测试防止错误账号悄悄获得本地登录状态。
数据流:测试让假发证服务器返回一个不被允许的工作区 ID,但启动登录服务器时只允许另一个 ID。随后模拟浏览器回调。页面会返回成功的 HTTP 响应,但内容是错误说明;服务器最终返回 PermissionDenied,也就是“没有权限”;磁盘上不会生成 auth.json。
调用关系:它用 start_mock_issuer 制造“不匹配的账号”,再用 run_login_server 启动带工作区限制的登录流程。它接在授权回调阶段,检查服务器拿到令牌后会不会做最后一道身份范围检查。
调用图:调用 2 个内部函数(new, start_mock_issuer);外部调用 7 个(assert!, assert_eq!, run_login_server, format!, skip_if_no_network!, tempdir, vec!)。
oauth_access_denied_missing_entitlement_blocks_login_with_clear_error319–385 ↗
async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error() -> Result<()>
作用:验证当 OAuth 回调明确说用户没有 Codex 权限时,登录会失败,并显示对普通用户友好的说明。这里的 entitlement 可以理解成“你有没有被开通使用资格”。
数据流:测试启动登录服务器后,不传 code,而是在回调地址里传 error=access_denied 和 error_description=missing_codex_entitlement。服务器返回一个错误页面,页面要说明“你没有 Codex 访问权限”以及“联系工作区管理员”。随后服务器结束并返回 PermissionDenied,且不会写 auth.json。
调用关系:它仍然用 start_mock_issuer 搭环境,但这条路径不会真的去换令牌,因为浏览器回调已经带了拒绝错误。它测试的是 run_login_server 的错误翻译能力:把底层 OAuth 错误变成用户看得懂的提示。
调用图:调用 2 个内部函数(new, start_mock_issuer);外部调用 6 个(assert!, assert_eq!, run_login_server, format!, skip_if_no_network!, tempdir)。
oauth_access_denied_unknown_reason_uses_generic_error_page388–466 ↗
async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<()>
作用:验证当 OAuth 拒绝登录但原因不是已知的 Codex 权限问题时,系统会显示通用错误页,同时保留原始错误细节,方便排查。
数据流:测试启动登录服务器,然后模拟回调里带 access_denied 和一个未知原因 some_other_reason。服务器返回的页面应该说“登录无法完成”,包含原始错误码和描述,但不能误报成“没有 Codex 权限”。最后服务器以 PermissionDenied 结束,并且不写 auth.json。
调用关系:它和上一条权限测试是一对:都经过 run_login_server 的 OAuth 错误回调入口,但一个检查特殊文案,一个检查通用文案。这样可以防止所有拒绝原因都被混成同一种提示。
调用图:调用 2 个内部函数(new, start_mock_issuer);外部调用 6 个(assert!, assert_eq!, run_login_server, format!, skip_if_no_network!, tempdir)。
falls_back_to_registered_fallback_port_when_default_port_is_in_use469–533 ↗
async fn falls_back_to_registered_fallback_port_when_default_port_is_in_use() -> Result<()>
作用:验证默认登录端口被别的程序占用时,登录服务器会自动改用登记好的备用端口。端口可以理解成电脑上不同网络服务的门牌号。
数据流:测试先确认备用端口空闲,再故意占住默认端口 1455,并开一个假的“不是 Codex”的服务器。然后启动 Codex 登录服务器。结果应该启动在备用端口 1457,并且生成的登录链接里的 redirect_uri,也就是登录后跳回来的地址,也要指向这个备用端口。最后测试取消服务器并等待它停下。
调用关系:它先用系统的 TcpListener 占位制造端口冲突,再调用 run_login_server 观察选择端口的行为,也会用 start_mock_issuer 提供登录配置。它覆盖的是服务启动阶段的容错逻辑,保证用户机器上端口被占时不会直接失败。
调用图:调用 3 个内部函数(default, new, start_mock_issuer);外部调用 13 个(new, from_secs, bind, assert!, assert_eq!, run_login_server, eprintln!, format!, skip_if_no_network!, tempdir (+3 more))。
cancels_previous_login_server_when_port_is_in_use536–599 ↗
async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()>
作用:验证如果同一个端口上已经有一个旧的 Codex 登录服务器,新启动的登录服务器能接管端口,并让旧服务器报告被取消。这样重复执行登录命令时不会互相卡住。
数据流:测试先启动第一个登录服务器,记下它实际使用的端口,并在后台等待它结束。稍等一下后,再用同一个端口启动第二个登录服务器。结果第二个服务器应成功占用该端口,第一个等待任务应收到 Interrupted,也就是“被中断”。最后测试访问 /cancel 取消第二个服务器,让它也正常结束为取消状态。
调用关系:它用 start_mock_issuer 准备共同的假登录来源,两次调用 run_login_server 来模拟“旧登录还没结束,又发起新登录”。它测试登录服务器之间的接力和清理关系,尤其是端口冲突时不要误杀普通服务,但要能取消旧的 Codex 登录服务。
调用图:调用 2 个内部函数(new, start_mock_issuer);外部调用 9 个(from_millis, assert!, assert_eq!, run_login_server, format!, skip_if_no_network!, tempdir, spawn, sleep)。
会话维护和登出
这些测试跟踪登录后的已认证状态,验证令牌刷新行为,以及登出时撤销并移除已保存认证信息的清理路径。
login/tests/suite/auth_refresh.rs源码 ↗
登录系统里有两类重要令牌:访问令牌,用来证明当前请求有权限;刷新令牌,用来换新的访问令牌。这个测试文件就像给登录续期流程做体检。它会临时建一个干净的用户目录,把认证信息写进 auth.json,再用 wiremock 搭一个假的服务器,假装自己是发新令牌的服务端。测试会检查:该刷新时真的发请求,不该刷新时完全不碰网络;刷新成功后,文件里的令牌和内存缓存都更新;如果磁盘上的登录信息被别的进程改了,要先重新读取,不能盲目覆盖;如果刷新令牌已经过期或被重复使用,要记住这是永久失败,不要一直重试。文件里还放了几个小工具:RefreshTokenTestContext 负责搭测试环境,EnvGuard 负责临时改环境变量并恢复,JWT 构造函数负责做带过期时间的假令牌。
refresh_token_succeeds_updates_storage37–110 ↗
async fn refresh_token_succeeds_updates_storage() -> Result<()>
作用:测试直接向认证服务器刷新令牌时,成功结果会同时写回磁盘和内存缓存。它还确认请求里带的是正确的 client_id 和旧刷新令牌。
数据流:进去的是一个假的认证服务器、临时用户目录、旧的访问令牌和刷新令牌 → 测试把旧认证信息写进 auth.json,然后调用刷新方法,假服务器返回新令牌 → 出来时 auth.json 里的 token 被换成新的,last_refresh 时间变新,AuthManager 的缓存也拿到同一份新令牌。
调用关系:这是刷新流程的基础成功用例。它先用 EnvGuard::set 改 client_id 环境变量,用 RefreshTokenTestContext::new 建测试环境,再用 build_tokens 准备旧令牌,最后驱动 AuthManager 的 refresh_token_from_authority,并检查服务器和本地存储。
调用图:调用 3 个内部函数(set, new, build_tokens);外部调用 11 个(days, given, start, new, now, assert!, assert_eq!, json!, skip_if_no_network!, method (+1 more))。
refresh_token_refreshes_when_auth_is_unchanged114–176 ↗
async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()>
作用:测试普通的 refresh_token 调用在本地认证信息没有被别人改过时,会正常刷新令牌。它覆盖的是更常用、更安全的刷新入口。
数据流:进去的是旧 auth.json 和一个会返回新令牌的假服务器 → 测试调用 AuthManager 的 refresh_token,让它自己判断能不能刷新 → 出来时磁盘和缓存里的访问令牌、刷新令牌都变成新值,刷新时间也推进。
调用关系:它和 refresh_token_succeeds_updates_storage 类似,但走的是 refresh_token 这个会先检查本地状态的入口。它依赖 RefreshTokenTestContext::new 搭环境,build_tokens 做测试令牌。
调用图:调用 2 个内部函数(new, build_tokens);外部调用 11 个(days, given, start, new, now, assert!, assert_eq!, json!, skip_if_no_network!, method (+1 more))。
auth_refreshes_when_access_token_is_near_expiry180–238 ↗
async fn auth_refreshes_when_access_token_is_near_expiry() -> Result<()>
作用:测试当访问令牌快过期时,只是调用 auth() 读取登录信息,也会自动触发刷新。这样用户不会等到请求真正失败才发现令牌没用了。
数据流:进去的是一个还没过期但只剩几分钟可用的访问令牌 → 测试把它写入 auth.json,然后调用 auth_manager.auth() → 出来时系统发现令牌快到期,向假服务器换来新令牌,并把磁盘和缓存都更新。
调用关系:这个测试验证 AuthManager 的自动续期行为。它用 access_token_with_expiration 造一个接近过期的假 JWT(JWT 是一段带信息的令牌字符串),再观察 auth() 是否把工作交给刷新流程。
调用图:调用 3 个内部函数(new, access_token_with_expiration, build_tokens);外部调用 11 个(minutes, given, start, new, now, assert!, assert_eq!, json!, skip_if_no_network!, method (+1 more))。
auth_skips_access_token_outside_refresh_window242–276 ↗
async fn auth_skips_access_token_outside_refresh_window() -> Result<()>
作用:测试访问令牌离过期还比较远时,auth() 不会多此一举去刷新。这样可以减少不必要的网络请求,也避免频繁更换令牌。
数据流:进去的是一个还有足够时间才过期的访问令牌 → 测试写入 auth.json 后调用 auth_manager.auth() → 出来时缓存仍是原令牌,磁盘文件完全没变,假服务器没有收到任何请求。
调用关系:它和 auth_refreshes_when_access_token_is_near_expiry 成对出现,一个证明快过期会刷新,一个证明还新鲜就不刷新。它同样依赖 access_token_with_expiration 和 build_tokens 做输入。
调用图:调用 3 个内部函数(new, access_token_with_expiration, build_tokens);外部调用 6 个(minutes, start, now, assert!, assert_eq!, skip_if_no_network!)。
refresh_token_skips_refresh_when_auth_changed280–337 ↗
async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()>
作用:测试如果内存里记着旧登录信息,但磁盘上的 auth.json 已经被别人改成另一套同账号令牌,刷新时应该先接受磁盘的新内容,而不是拿旧刷新令牌去换。这样能避免覆盖别的进程刚写好的登录状态。
数据流:进去的是内存中的旧令牌,以及后来手动写到磁盘上的另一套令牌 → 调用 refresh_token 后,系统发现磁盘内容已变化 → 出来时磁盘保持新内容不变,缓存改成磁盘上的令牌,服务器没有收到刷新请求。
调用关系:这个测试把 save_auth 直接用于磁盘,绕过 AuthManager,模拟外部修改。然后调用 AuthManager 的 refresh_token,检查它是否选择重新加载,而不是继续调用远端刷新。
调用图:调用 3 个内部函数(default, new, build_tokens);外部调用 7 个(days, start, now, assert!, assert_eq!, save_auth, skip_if_no_network!)。
refresh_token_errors_on_account_mismatch341–412 ↗
async fn refresh_token_errors_on_account_mismatch() -> Result<()>
作用:测试如果磁盘上的认证信息变成了另一个账号,刷新流程必须报错,不能悄悄切账号。账号不匹配是很危险的情况,可能导致把 A 用户的会话变成 B 用户。
数据流:进去的是缓存里的原账号令牌,以及磁盘上一份 account_id 不同的新令牌 → 调用 refresh_token 后,系统比较账号发现不一致 → 出来的是一个 Other 类型失败,磁盘保持另一个账号内容,缓存仍保留原账号令牌,服务器没有被调用。
调用关系:它延续“磁盘内容被改”的场景,但把同账号改成不同账号。它使用 build_tokens 后手动改 account_id,借 AuthManager 的 refresh_token 验证账号保护逻辑。
调用图:调用 3 个内部函数(default, new, build_tokens);外部调用 12 个(days, given, start, new, now, assert!, assert_eq!, save_auth, json!, skip_if_no_network! (+2 more))。
returns_fresh_tokens_as_is416–461 ↗
async fn returns_fresh_tokens_as_is() -> Result<()>
作用:测试即使上次刷新时间已经很久以前,只要访问令牌本身还很新,auth() 就直接返回现有令牌。它说明系统看的是实际可用性,不只是刷新时间。
数据流:进去的是一个 last_refresh 很旧、但访问令牌还有一小时才过期的 auth.json → 调用 auth_manager.auth() → 出来时令牌原样返回,磁盘不变,假服务器没有收到请求。
调用关系:这个测试用 access_token_with_expiration 制造长寿命访问令牌,用来确认自动刷新窗口只针对快过期或已过期的访问令牌,而不是简单按 last_refresh 老旧就刷新。
调用图:调用 3 个内部函数(new, access_token_with_expiration, build_tokens);外部调用 12 个(days, hours, given, start, new, now, assert!, assert_eq!, json!, skip_if_no_network! (+2 more))。
refreshes_token_when_access_token_is_expired465–523 ↗
async fn refreshes_token_when_access_token_is_expired() -> Result<()>
作用:测试访问令牌已经过期时,读取 auth() 会立刻刷新。没有这个行为,用户可能拿着过期令牌继续发请求,然后不断遇到未授权错误。
数据流:进去的是一个过期一小时的访问令牌和仍可用的刷新令牌 → 调用 auth_manager.auth() 后,系统向假服务器请求新令牌 → 出来时缓存和 auth.json 都换成新访问令牌、新刷新令牌,刷新时间推进。
调用关系:它是自动刷新路径里的过期场景。它使用 access_token_with_expiration 造过期 token,使用假服务器确认 AuthManager 真的发起了一次刷新。
调用图:调用 3 个内部函数(new, access_token_with_expiration, build_tokens);外部调用 12 个(days, hours, given, start, new, now, assert!, assert_eq!, json!, skip_if_no_network! (+2 more))。
auth_reloads_disk_auth_when_cached_auth_is_stale527–581 ↗
async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()>
作用:测试内存缓存看起来过旧时,如果磁盘上已经有更新鲜的认证信息,auth() 会重新读取磁盘,而不是直接刷新旧缓存。这样多个进程共用登录文件时更安全。
数据流:进去的是缓存里的旧 auth.json,以及后来直接写入磁盘的新 auth.json → 调用 auth_manager.auth() → 出来时缓存变成磁盘上的新令牌,磁盘内容保持不变,网络请求为零。
调用关系:它模拟外部进程更新了 auth.json。测试先通过 RefreshTokenTestContext::write_auth 建缓存,再用 save_auth 改磁盘,最后观察 auth() 是否走重新加载路径。
调用图:调用 3 个内部函数(default, new, build_tokens);外部调用 7 个(days, start, now, assert!, assert_eq!, save_auth, skip_if_no_network!)。
auth_reloads_disk_auth_without_calling_expired_refresh_token585–647 ↗
async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Result<()>
作用:测试当缓存旧了、磁盘已有新认证信息时,即使服务器那边会说旧刷新令牌已过期,系统也不应该去调用它。重点是避免用已经被替换的旧令牌制造错误。
数据流:进去的是旧缓存、磁盘上的新令牌,以及一个如果被调用就返回“刷新令牌过期”的假服务器 → 调用 auth() → 出来时缓存从磁盘更新,服务器请求次数仍是零。
调用关系:这个测试和 auth_reloads_disk_auth_when_cached_auth_is_stale 类似,但额外设置了一个不该被碰的失败接口。它用服务器的 expect(0) 来证明 AuthManager 优先重新加载磁盘。
调用图:调用 3 个内部函数(default, new, build_tokens);外部调用 11 个(days, given, start, new, now, assert_eq!, save_auth, json!, skip_if_no_network!, method (+1 more))。
refresh_token_returns_permanent_error_for_expired_refresh_token651–702 ↗
async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Result<()>
作用:测试服务器明确说刷新令牌过期时,刷新函数会把它识别成永久失败。永久失败的意思是重试也没用,通常需要用户重新登录。
数据流:进去的是一份正常缓存的令牌,以及返回 401 和 refresh_token_expired 的假服务器 → 调用 refresh_token_from_authority → 出来的是失败原因 Expired,磁盘和缓存都保持旧令牌不变。
调用关系:它直接测试远端刷新失败分类。假服务器用固定错误响应,AuthManager 负责把服务端错误翻译成 RefreshTokenFailedReason::Expired。
调用图:调用 2 个内部函数(new, build_tokens);外部调用 10 个(days, given, start, new, now, assert_eq!, json!, skip_if_no_network!, method, path)。
refresh_token_does_not_retry_after_permanent_failure706–771 ↗
async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()>
作用:测试刷新令牌被判定为永久失效后,第二次调用不会再打服务器。这样能避免一直用坏令牌刷接口,也减少无意义请求。
数据流:进去的是一份旧令牌,以及会返回 refresh_token_reused 的假服务器 → 第一次 refresh_token 得到 Exhausted 失败并记录下来 → 第二次 refresh_token 直接返回同类失败,不再请求服务器,磁盘和缓存不变。
调用关系:它验证 AuthManager 对永久失败有记忆。服务器只允许一次请求,测试通过第二次仍失败且 verify 成功来确认没有重复联网。
调用图:调用 2 个内部函数(new, build_tokens);外部调用 10 个(days, given, start, new, now, assert_eq!, json!, skip_if_no_network!, method, path)。
refresh_token_does_not_retry_after_bad_request_reused_failure775–840 ↗
async fn refresh_token_does_not_retry_after_bad_request_reused_failure() -> Result<()>
作用:测试即使服务器用 400 Bad Request 返回 refresh_token_reused,系统也把它当作永久失败,并且之后不重试。这里的 400 是一种“请求本身不被接受”的 HTTP 状态码。
数据流:进去的是旧令牌,以及返回 400 加 refresh_token_reused 的假服务器 → 第一次刷新失败并标记为 Exhausted → 第二次刷新直接失败,不再发网络请求,认证文件不被改动。
调用关系:它和 refresh_token_does_not_retry_after_permanent_failure 检查同一个安全策略,但覆盖不同 HTTP 状态码。它确保错误判断不只依赖 401。
调用图:调用 2 个内部函数(new, build_tokens);外部调用 10 个(days, given, start, new, now, assert_eq!, json!, skip_if_no_network!, method, path)。
refresh_token_reloads_changed_auth_after_permanent_failure844–928 ↗
async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<()>
作用:测试曾经发生永久刷新失败后,如果磁盘上的认证信息后来变了,系统可以重新加载这份新信息,而不是永远卡在旧失败状态。这样用户重新登录或别的进程修复登录后,程序能恢复。
数据流:进去的是一份会刷新失败的旧令牌 → 第一次 refresh_token 得到 Exhausted → 然后测试直接把新令牌写入磁盘 → 第二次 refresh_token 发现认证信息已变化,只重新加载磁盘,不再打服务器,缓存变成新令牌。
调用关系:它把永久失败记忆和磁盘变更检测放在一起测。AuthManager 需要既避免重试坏令牌,又不能忽略后来出现的新 auth.json。
调用图:调用 3 个内部函数(default, new, build_tokens);外部调用 12 个(days, hours, given, start, new, now, assert_eq!, save_auth, json!, skip_if_no_network! (+2 more))。
refresh_token_returns_transient_error_on_server_failure932–982 ↗
async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()>
作用:测试服务器临时故障时,刷新函数返回临时错误,而不是把令牌判死刑。临时错误表示以后再试可能会好,比如服务器 500。
数据流:进去的是旧令牌,以及返回 500 temporary-failure 的假服务器 → 调用 refresh_token_from_authority → 出来的是 RefreshTokenError::Transient,失败原因没有固定枚举,磁盘和缓存都保持旧值。
调用关系:它检查错误分类的另一面:不是所有刷新失败都该阻止重试。AuthManager 要把服务器故障和刷新令牌失效区分开。
调用图:调用 2 个内部函数(new, build_tokens);外部调用 11 个(days, given, start, new, now, assert!, assert_eq!, json!, skip_if_no_network!, method (+1 more))。
unauthorized_recovery_reloads_then_refreshes_tokens986–1081 ↗
async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()>
作用:测试遇到未授权后的一套恢复步骤:先重新读磁盘,看别人是否已经更新登录;如果还需要,再刷新令牌。这样能用最温和的办法修复 401 未授权问题。
数据流:进去的是缓存里的旧令牌、磁盘上后来写入的新令牌,以及能返回恢复令牌的假服务器 → 第一次 recovery.next() 只重新加载磁盘,缓存变成磁盘令牌且不联网 → 第二次 next() 再向服务器刷新,磁盘和缓存变成 recovered 令牌,恢复流程结束。
调用关系:它测试 AuthManager::unauthorized_recovery 产生的分步恢复器。这个恢复器像一个两步按钮:先 reload,再 refresh;测试用 has_next 和 next 验证顺序。
调用图:调用 3 个内部函数(default, new, build_tokens);外部调用 12 个(days, given, start, new, now, assert!, assert_eq!, save_auth, json!, skip_if_no_network! (+2 more))。
unauthorized_recovery_errors_on_account_mismatch1085–1167 ↗
async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()>
作用:测试未授权恢复时,如果磁盘上的认证信息属于另一个账号,恢复必须停止并报错。这样不会因为自动恢复而偷偷切换用户身份。
数据流:进去的是缓存中的原账号令牌,以及磁盘上 account_id 不同的令牌 → 调用 recovery.next() 尝试第一步重新加载 → 出来的是 Other 失败,磁盘保持不变,缓存仍是原账号,服务器没有被调用。
调用关系:它是 unauthorized_recovery_reloads_then_refreshes_tokens 的危险分支。恢复器在把磁盘内容放进缓存前必须先做账号一致性检查。
调用图:调用 3 个内部函数(default, new, build_tokens);外部调用 12 个(days, given, start, new, now, assert!, assert_eq!, save_auth, json!, skip_if_no_network! (+2 more))。
unauthorized_recovery_requires_chatgpt_auth1171–1201 ↗
async fn unauthorized_recovery_requires_chatgpt_auth() -> Result<()>
作用:测试未授权恢复只适用于 ChatGPT 登录令牌,不适用于 API Key 登录。API Key 没有刷新令牌,所以不能走同一套恢复流程。
数据流:进去的是一份 AuthMode::ApiKey 的 auth.json,里面只有 openai_api_key 没有 token → 创建 unauthorized_recovery 后发现没有可执行步骤 → 强行 next() 会返回 Other 失败,服务器没有收到请求。
调用关系:它确保恢复器只在正确登录模式下工作。测试通过 RefreshTokenTestContext::new 建环境,然后写入 API Key 型认证信息。
调用图:调用 1 个内部函数(new);外部调用 4 个(start, assert!, assert_eq!, skip_if_no_network!)。
RefreshTokenTestContext::new1210–1230 ↗
async fn new(server: &MockServer) -> Result<Self>
作用:创建每个测试需要的干净小世界:临时用户目录、假的刷新地址、以及指向这个目录的 AuthManager。这样每个测试互不污染。
数据流:进去的是一个 wiremock 假服务器 → 函数创建临时目录,把刷新令牌接口环境变量改成假服务器地址,再初始化 AuthManager → 出来的是 RefreshTokenTestContext,里面带目录、认证管理器和环境变量保护器。
调用关系:几乎所有测试都先调用它。它内部会调用 EnvGuard::set 临时改环境变量,并调用 AuthManager::shared 创建被测试对象。
调用图:调用 3 个内部函数(default, shared, set);被 18 处调用(auth_refreshes_when_access_token_is_near_expiry, auth_reloads_disk_auth_when_cached_auth_is_stale, auth_reloads_disk_auth_without_calling_expired_refresh_token, auth_skips_access_token_outside_refresh_window, refresh_token_does_not_retry_after_bad_request_reused_failure, refresh_token_does_not_retry_after_permanent_failure, refresh_token_errors_on_account_mismatch, refresh_token_refreshes_when_auth_is_unchanged, refresh_token_reloads_changed_auth_after_permanent_failure, refresh_token_returns_permanent_error_for_expired_refresh_token (+8 more));外部调用 2 个(new, format!)。
RefreshTokenTestContext::load_auth1232–1240 ↗
fn load_auth(&self) -> Result<AuthDotJson>
作用:从测试的临时用户目录里读取 auth.json。测试用它确认磁盘上的认证信息最后到底变成了什么。
数据流:进去的是 RefreshTokenTestContext 自己保存的临时目录路径 → 函数调用 load_auth_dot_json 按文件存储模式读取认证文件 → 出来的是 AuthDotJson;如果文件不存在或读取失败,就返回带上下文的错误。
调用关系:各个测试在刷新、重载或失败之后调用它做验收。它不改数据,只负责把当前磁盘状态拿出来给断言比较。
调用图:调用 1 个内部函数(default);外部调用 2 个(path, load_auth_dot_json)。
RefreshTokenTestContext::write_auth1242–1251 ↗
async fn write_auth(&self, auth_dot_json: &AuthDotJson) -> Result<()>
作用:把指定认证信息写入测试目录,并让 AuthManager 重新加载。它用于给每个测试安排初始登录状态。
数据流:进去的是一份 AuthDotJson → 函数把它保存到临时目录的 auth.json,然后调用 auth_manager.reload() 刷新内存缓存 → 出来时磁盘和缓存都处在测试指定的状态。
调用关系:测试通常先用 build_tokens 构造令牌,再用 write_auth 放进环境。它把底层 save_auth 和 AuthManager 的 reload 串起来,免得每个测试重复写这些步骤。
EnvGuard::set1260–1267 ↗
fn set(key: &'static str, value: String) -> Self
作用:临时设置一个环境变量,并记住原来的值,方便测试结束后恢复。环境变量可以理解成进程级别的小开关,会影响 AuthManager 用哪个地址或 client_id。
数据流:进去的是环境变量名和值 → 函数先读取旧值,再把新值写进当前进程环境 → 出来的是 EnvGuard,里面保存变量名和旧值。
调用关系:RefreshTokenTestContext::new 用它把刷新地址指向假服务器;refresh_token_succeeds_updates_storage 也用它改 client_id。因为测试用 serial 串行执行,所以这样改全局环境才安全。
调用图:被 6 处调用(new, refresh_token_succeeds_updates_storage, auth_manager_logout_with_revoke_uses_cached_auth, logout_with_revoke_removes_auth_when_revoke_fails, logout_with_revoke_revokes_refresh_token_then_removes_auth, logout_with_revoke_uses_stored_auth_when_access_token_env_is_set);外部调用 2 个(set_var, var_os)。
EnvGuard::drop1271–1279 ↗
fn drop(&mut self)
作用:在 EnvGuard 被丢弃时自动恢复环境变量。它像“借完东西放回原位”的保险,避免一个测试改的环境影响下一个测试。
数据流:进去的是 EnvGuard 里保存的变量名和原始值 → 如果原来有值,就设回原值;如果原来没有,就删除这个变量 → 出来时进程环境恢复到测试前状态。
调用关系:它由 Rust 的 Drop 机制自动调用,测试代码不用手写清理。EnvGuard::set 创建的保护器离开作用域时,这个函数就完成收尾。
调用图:外部调用 2 个(remove_var, set_var)。
jwt_with_payload1282–1304 ↗
fn jwt_with_payload(payload: serde_json::Value) -> String
作用:生成一个测试用的假 JWT 字符串,里面可以塞入指定内容,比如用户 id 或过期时间。JWT 是一种用点号分成三段的令牌格式。
数据流:进去的是一段 JSON 内容作为 payload → 函数做一个固定头部,把头部、payload 和假签名分别转成 URL 安全的 base64 字符串,再用点号拼起来 → 出来的是形如 header.payload.signature 的令牌字符串。
调用关系:它是造假令牌的底层工具。minimal_jwt 和 access_token_with_expiration 都调用它,测试不会用它做真实安全校验,只需要格式能被代码解析。
调用图:被 2 处调用(access_token_with_expiration, minimal_jwt);外部调用 2 个(format!, to_vec)。
minimal_jwt1306–1308 ↗
fn minimal_jwt() -> String
作用:生成一个最简单的假 JWT,只带用户标识 sub。它用于填充 id_token,让 TokenData 看起来像一份完整登录令牌。
数据流:进去没有外部参数 → 函数准备一个包含 sub: user-123 的 JSON,然后交给 jwt_with_payload → 出来的是一个没有过期时间的测试 JWT 字符串。
调用关系:build_tokens 会调用它来创建 id_token。它把测试里反复需要的最小身份令牌封装起来。
调用图:调用 1 个内部函数(jwt_with_payload);被 1 处调用(build_tokens);外部调用 1 个(json!)。
access_token_with_expiration1310–1312 ↗
fn access_token_with_expiration(expires_at: chrono::DateTime<Utc>) -> String
作用:生成一个带过期时间的假访问令牌。测试用它模拟“快过期”“还很新”“已经过期”三种情况。
数据流:进去的是一个具体过期时间 expires_at → 函数把用户标识和 exp 时间戳放进 JSON,再调用 jwt_with_payload → 出来的是带 exp 字段的访问令牌字符串。
调用关系:自动刷新相关测试会调用它。AuthManager 读取这个令牌里的 exp 后,决定要不要刷新。
调用图:调用 1 个内部函数(jwt_with_payload);被 4 处调用(auth_refreshes_when_access_token_is_near_expiry, auth_skips_access_token_outside_refresh_window, refreshes_token_when_access_token_is_expired, returns_fresh_tokens_as_is);外部调用 1 个(json!)。
build_tokens1314–1325 ↗
fn build_tokens(access_token: &str, refresh_token: &str) -> TokenData
作用:快速组装一整套测试用 TokenData,包括 id_token、访问令牌、刷新令牌和账号 id。这样每个测试不用重复手写同样结构。
数据流:进去的是访问令牌字符串和刷新令牌字符串 → 函数先用 minimal_jwt 做 id_token,再把传入的两个令牌和固定 account_id 填进 TokenData → 出来的是一份可写入 auth.json 的完整令牌数据。
调用关系:大多数测试都用它准备初始令牌或磁盘令牌。它是测试数据工厂,配合 RefreshTokenTestContext::write_auth 或 save_auth 使用。
调用图:调用 1 个内部函数(minimal_jwt);被 17 处调用(auth_refreshes_when_access_token_is_near_expiry, auth_reloads_disk_auth_when_cached_auth_is_stale, auth_reloads_disk_auth_without_calling_expired_refresh_token, auth_skips_access_token_outside_refresh_window, refresh_token_does_not_retry_after_bad_request_reused_failure, refresh_token_does_not_retry_after_permanent_failure, refresh_token_errors_on_account_mismatch, refresh_token_refreshes_when_auth_is_unchanged, refresh_token_reloads_changed_auth_after_permanent_failure, refresh_token_returns_permanent_error_for_expired_refresh_token (+7 more));外部调用 1 个(default)。
login/tests/suite/logout.rs源码 ↗
退出登录看起来只是删个文件,但这里其实有两件重要的事:先把刷新令牌发给撤销接口,让服务器知道这个令牌不能再换新登录票据;然后再删除本机的 auth.json 登录文件。这个测试文件用临时目录假装用户的 Codex 主目录,用 wiremock 这个“假服务器”接住撤销请求,检查请求内容是否正确。它还故意测试撤销失败的情况:就算服务器返回 500,本地登录信息也必须删掉,避免用户被卡在“退不出去”的状态。文件里的 EnvGuard 像临时贴纸一样改环境变量,测试结束自动贴回原样,避免影响别的测试。
logout_with_revoke_revokes_refresh_token_then_removes_auth34–87 ↗
async fn logout_with_revoke_revokes_refresh_token_then_removes_auth() -> Result<()>
作用:测试最标准的退出流程:程序应该把保存的刷新令牌发给服务器撤销,然后删掉本地 auth.json。它还确认撤销请求里带的是正确的 token、token 类型和 client_id。
数据流:测试先临时设置 client_id 和撤销接口地址,再启动一个假服务器等待一次 POST 请求。接着在临时目录写入一份假的 ChatGPT 登录信息,然后调用 logout_with_revoke。调用之后,它检查返回值表示确实删除了登录信息、auth.json 已经不存在,并读取假服务器收到的请求,确认请求 JSON 正是预期内容。
调用关系:这是直接验证 logout_with_revoke 的主路径测试。它用 EnvGuard::set 临时改环境变量,用 chatgpt_auth 准备登录文件,假服务器收到请求后由测试断言整个退出流程是否按顺序完成。
调用图:调用 3 个内部函数(default, set, chatgpt_auth);外部调用 13 个(given, start, new, new, assert!, assert_eq!, logout_with_revoke, save_auth, format!, json! (+3 more))。
logout_with_revoke_uses_stored_auth_when_access_token_env_is_set91–129 ↗
async fn logout_with_revoke_uses_stored_auth_when_access_token_env_is_set() -> Result<()>
作用:测试即使环境变量里有一个访问令牌,退出登录也应该使用磁盘上保存的完整登录信息来撤销和删除。这样可以避免环境变量干扰真正需要清理的本地账号。
数据流:测试先启动假撤销服务器,并设置撤销地址。然后额外设置 CODEX_ACCESS_TOKEN_ENV_VAR,模拟外部环境里有访问令牌。它再把假的 auth.json 写入临时目录,调用 logout_with_revoke,最后确认本地 auth.json 被删掉,并确认假服务器确实收到了一次撤销请求。
调用关系:这个测试仍然围绕 logout_with_revoke,但重点是环境变量和磁盘登录信息同时存在时的取舍。它借助 EnvGuard::set 制造环境变量场景,借助 chatgpt_auth 写入可撤销的本地凭证。
调用图:调用 3 个内部函数(default, set, chatgpt_auth);外部调用 11 个(given, start, new, new, assert!, logout_with_revoke, save_auth, format!, skip_if_no_network!, method (+1 more))。
logout_with_revoke_removes_auth_when_revoke_fails133–172 ↗
async fn logout_with_revoke_removes_auth_when_revoke_fails() -> Result<()>
作用:测试服务器撤销令牌失败时,程序也不能放弃本地退出。换句话说,远端清理失败了,本机登录文件仍然要删除。
数据流:测试搭起一个会返回 500 错误的假服务器,设置撤销接口地址,然后在临时目录保存一份假的登录信息。调用 logout_with_revoke 后,它检查函数仍然返回“已删除”,并确认 auth.json 不存在。最后让假服务器核对确实收到过那次撤销请求。
调用关系:这是 logout_with_revoke 的容错测试。它用 chatgpt_auth 生成测试凭证,用 EnvGuard::set 指向假服务器,专门验证撤销失败不会阻止本地清理。
调用图:调用 3 个内部函数(default, set, chatgpt_auth);外部调用 12 个(given, start, new, new, assert!, logout_with_revoke, save_auth, format!, json!, skip_if_no_network! (+2 more))。
auth_manager_logout_with_revoke_uses_cached_auth176–238 ↗
async fn auth_manager_logout_with_revoke_uses_cached_auth() -> Result<()>
作用:测试 AuthManager 这个登录管理器退出时,会使用自己已经缓存的登录信息,而不是后来磁盘上被改过的新内容。这样可以保证管理器按它当前掌握的账号状态执行退出。
数据流:测试先写入一份带旧刷新令牌的登录文件,再创建 AuthManager,让它把这份信息读进缓存。随后测试故意把磁盘上的 auth.json 改成另一个刷新令牌。调用 manager.logout_with_revoke 后,它检查管理器缓存被清空、本地文件被删除,并查看假服务器收到的请求,确认撤销的是最初缓存里的 REFRESH_TOKEN,而不是后来写到磁盘的新令牌。
调用关系:这个测试不直接调用普通的 logout_with_revoke,而是走 AuthManager::logout_with_revoke。它用 chatgpt_auth_with_refresh_token 制造两个不同版本的登录信息,用假服务器证明管理器选择的是缓存里的那一份。
调用图:调用 4 个内部函数(default, new, set, chatgpt_auth_with_refresh_token);外部调用 12 个(given, start, new, new, assert!, assert_eq!, save_auth, format!, json!, skip_if_no_network! (+2 more))。
chatgpt_auth240–242 ↗
fn chatgpt_auth() -> AuthDotJson
作用:生成一份默认的假 ChatGPT 登录信息,供多个测试重复使用。它省掉每个测试都手写完整 auth.json 内容的麻烦。
数据流:它没有外部输入,内部使用固定的 REFRESH_TOKEN。它把这个固定刷新令牌交给 chatgpt_auth_with_refresh_token,拿回一份完整的 AuthDotJson 测试登录对象。
调用关系:这是测试数据的小工厂。几个 logout_with_revoke 测试需要一份标准登录文件时,会调用它;真正组装字段的工作交给 chatgpt_auth_with_refresh_token。
调用图:调用 1 个内部函数(chatgpt_auth_with_refresh_token);被 3 处调用(logout_with_revoke_removes_auth_when_revoke_fails, logout_with_revoke_revokes_refresh_token_then_removes_auth, logout_with_revoke_uses_stored_auth_when_access_token_env_is_set)。
chatgpt_auth_with_refresh_token244–262 ↗
fn chatgpt_auth_with_refresh_token(refresh_token: &str) -> AuthDotJson
作用:生成一份可指定刷新令牌的假登录信息。测试用它来区分“旧令牌”和“新令牌”,从而确认程序到底用了哪一份凭证。
数据流:输入是一个刷新令牌字符串。函数创建 AuthDotJson,里面标明登录模式是 ChatGPT,填入访问令牌、刷新令牌、账号 ID 和一个最小可用的身份令牌。身份令牌由 minimal_jwt 生成。输出是一整份可保存到 auth.json 的测试登录对象。
调用关系:它被 chatgpt_auth 包装成默认版本,也被 AuthManager 缓存测试直接调用。它负责准备测试数据,真正的退出逻辑会读取这些数据并拿刷新令牌去撤销。
调用图:调用 1 个内部函数(minimal_jwt);被 2 处调用(auth_manager_logout_with_revoke_uses_cached_auth, chatgpt_auth);外部调用 1 个(default)。
minimal_jwt264–270 ↗
fn minimal_jwt() -> String
作用:生成一个很小的假 JWT 身份令牌。JWT 可以理解成由三段文本拼起来的身份票据,这里只需要它长得像真的,方便测试通过解析。
数据流:它没有输入。函数把一个简单 header、一个包含用户 sub 的 payload、以及一个假签名分别做 URL 安全的 base64 编码,再用点号拼成 header.payload.signature 的格式。输出是这个假 JWT 字符串。
调用关系:它只服务于 chatgpt_auth_with_refresh_token。测试登录对象需要一个 id_token 字段时,它提供最小但格式正确的内容,不参与真正的网络撤销流程。
调用图:被 1 处调用(chatgpt_auth_with_refresh_token);外部调用 1 个(format!)。
EnvGuard::set278–285 ↗
fn set(key: &'static str, value: String) -> Self
作用:临时设置一个环境变量,并记住它原来的值。它像给测试环境贴一张临时标签,测试结束后可以自动恢复。
数据流:输入是环境变量名和新值。函数先读取这个变量原本有没有值,然后把它改成新值,最后返回一个 EnvGuard 对象,里面保存变量名和原始值。这个返回对象之后会负责恢复现场。
调用关系:多个测试用它来临时改 client_id、撤销接口地址或访问令牌环境变量。它和 EnvGuard::drop 配套使用:set 负责改,drop 负责还原,避免一个测试污染下一个测试。
EnvGuard::drop289–297 ↗
fn drop(&mut self)
作用:在 EnvGuard 离开作用范围时自动恢复环境变量。这样测试即使中途失败,也尽量不会把全局环境弄乱。
数据流:它读取 EnvGuard 里保存的变量名和原始值。如果原来有值,就把环境变量设回原值;如果原来没有值,就把这个变量删掉。它没有返回值,但会改变当前进程的环境变量。
调用关系:它是 EnvGuard::set 的收尾动作,由 Rust 在对象被释放时自动调用。因为这些测试被标记为串行执行,所以这种修改全局环境变量的做法不会和同组测试同时打架。
调用图:外部调用 2 个(remove_var, set_var)。