Files
codex-app-server/cloud/tests/test_auth.py
T
host 5db89d1b06 code-to10 v6 A4: fix AuthStore.audit NameError (import sqlite3) + retry regression test
Pre-fix any first-attempt audit failure raised NameError at
auth.py:424 (sqlite3 never imported), aborting the 5-attempt
retry loop on attempt 1 and masking the original error. The
424-line coverage mystery is also explained: the only full-suite
hit is test_child_model.test_coordinator_inherits_child_defaults
via worker best-effort _audit swallowing an OperationalError
(database is locked); 422-423/426-427 stay uncovered because
rollback succeeds and NameError aborts before either raise.
Bubble audit (see report): no merge/release gate swallows the
NameError and proceeds, so this stays a downgrade, not a
production blocker.

(cherry picked from commit 039c6adbac3b120b5b9a1dcbafeff6a91407ce70)
2026-11-03 02:49:26 +00:00

223 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""auth 模块单测:API key 认证、角色权限、审计只增不减。"""
import os
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.auth import ROLES, AuthStore
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import ROLES, AuthStore
class TestAuth(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.store = AuthStore(os.path.join(self.tmp.name, "auth.db"))
def tearDown(self):
self.tmp.cleanup()
def _user(self, username):
return self.store.create_user(username)
def _project_with_roles(self):
"""owner + viewer + contributor + maintainer + 一个非成员,返回其 id。"""
owner_id, _ = self._user("owner")
viewer_id, _ = self._user("viewer")
contributor_id, _ = self._user("contributor")
maintainer_id, _ = self._user("maintainer")
outsider_id, _ = self._user("outsider")
project_id = "proj-1"
self.store.create_project(project_id, owner_id)
self.store.set_member(project_id, owner_id, viewer_id, "viewer")
self.store.set_member(project_id, owner_id, contributor_id, "contributor")
self.store.set_member(project_id, owner_id, maintainer_id, "maintainer")
return project_id, owner_id, viewer_id, contributor_id, maintainer_id, outsider_id
def test_create_user_plain_key_authenticates(self):
user_id, api_key_plain = self._user("alice")
self.assertEqual(self.store.authenticate(api_key_plain), user_id)
def test_wrong_key_rejected(self):
user_id, api_key_plain = self._user("alice")
self.assertIsNone(self.store.authenticate(api_key_plain + "x"))
self.assertIsNone(self.store.authenticate("not-a-real-key"))
self.assertIsNone(self.store.authenticate(""))
self.assertIsNone(self.store.authenticate(None))
def test_hash_stored_not_plaintext(self):
_user_id, api_key_plain = self._user("alice")
db_path = os.path.join(self.tmp.name, "auth.db")
conn = sqlite3.connect(db_path)
try:
row = conn.execute(
"SELECT api_key_hash FROM users WHERE username = ?", ("alice",)
).fetchone()
finally:
conn.close()
self.assertIsNotNone(row)
stored = row[0]
self.assertNotEqual(stored, api_key_plain)
self.assertNotIn(api_key_plain, stored)
# 库里只有哈希列,没有明文列可查
columns = [c[1] for c in sqlite3.connect(db_path).execute(
"PRAGMA table_info(users)").fetchall()]
self.assertNotIn("api_key_plain", columns)
self.assertNotIn("api_key", columns)
def test_permission_denials(self):
project_id, _o, viewer_id, contributor_id, _m, outsider_id = (
self._project_with_roles()
)
# viewer 不能 create_task
self.assertFalse(
self.store.check_permission(viewer_id, project_id, "create_task"))
with self.assertRaises(PermissionError):
self.store.require_permission(viewer_id, project_id, "create_task")
# contributor 不能 approve_merge
self.assertFalse(
self.store.check_permission(contributor_id, project_id, "approve_merge"))
with self.assertRaises(PermissionError):
self.store.require_permission(
contributor_id, project_id, "approve_merge")
# 非成员读不到
self.assertFalse(
self.store.check_permission(outsider_id, project_id, "read"))
self.assertFalse(
self.store.check_permission(outsider_id, project_id, "create_task"))
with self.assertRaises(PermissionError):
self.store.require_permission(outsider_id, project_id, "read")
def test_unknown_role_denied_not_500(self):
# 回归:project_members 里直接写库留下的未知角色(如
# 'member',set_member 本身会拦,但旧行/直写库拦不住)
# 在生产上曾让 check_permission 以 KeyError 冒泡成 500。
# 权限检查的失败语义只能是 False / PermissionError。
project_id, _o, viewer_id, _c, _m, _outsider = (
self._project_with_roles()
)
db_path = os.path.join(self.tmp.name, "auth.db")
conn = sqlite3.connect(db_path)
try:
conn.execute(
"UPDATE project_members SET role = ?"
" WHERE project_id = ? AND user_id = ?",
("member", project_id, viewer_id))
conn.commit()
finally:
conn.close()
self.assertFalse(
self.store.check_permission(viewer_id, project_id, "read"))
self.assertFalse(
self.store.check_permission(viewer_id, project_id, "create_task"))
with self.assertRaises(PermissionError):
self.store.require_permission(viewer_id, project_id, "read")
def test_permission_grants(self):
project_id, owner_id, viewer_id, contributor_id, maintainer_id, _ = (
self._project_with_roles()
)
self.assertTrue(self.store.check_permission(viewer_id, project_id, "read"))
self.assertTrue(
self.store.check_permission(contributor_id, project_id, "create_task"))
self.assertTrue(
self.store.check_permission(contributor_id, project_id, "read"))
self.assertTrue(
self.store.check_permission(maintainer_id, project_id, "approve_merge"))
self.assertTrue(
self.store.check_permission(owner_id, project_id, "manage_project"))
self.assertFalse(
self.store.check_permission(maintainer_id, project_id, "manage_project"))
self.assertEqual(
ROLES, ["viewer", "contributor", "maintainer", "owner"])
def test_owner_can_set_member_maintainer_cannot(self):
project_id, owner_id, _v, _c, maintainer_id, _o = (
self._project_with_roles()
)
target_id, _ = self._user("target")
# owner 改成员成功
self.store.set_member(project_id, owner_id, target_id, "viewer")
self.assertTrue(self.store.check_permission(target_id, project_id, "read"))
self.assertFalse(
self.store.check_permission(target_id, project_id, "create_task"))
# maintainer 改成员被拒,且原角色不变
with self.assertRaises(PermissionError):
self.store.set_member(
project_id, maintainer_id, target_id, "contributor")
self.assertFalse(
self.store.check_permission(target_id, project_id, "create_task"))
def test_audit_retries_integrity_error_and_reraises_others(self):
"""A4 回归:audit() 重试分支的 sqlite3 引用必须存在。
首调 IntegrityError 后重试成功:审计对恰好一行(audit_log
1 行 + audit_ledger 1 行),不是 0 行也不是多行;非完整
性错误原样抛回(RuntimeError,不是 NameError 掩盖原错)。
背景:auth.py 曾全文件无 import sqlite3,audit() 的
except 分支在 424 行求 isinstance(exc, sqlite3.IntegrityError)
时直接 NameError,任何首调失败都走不到重试或原错抛回。
"""
import cloud.auth as auth_mod
original = auth_mod.audit_on_conn
calls = {"n": 0}
def _flaky(conn, *args, **kwargs):
calls["n"] += 1
if calls["n"] == 1:
raise sqlite3.IntegrityError(
"UNIQUE constraint failed: audit_ledger.entry_hash")
return original(conn, *args, **kwargs)
auth_mod.audit_on_conn = _flaky
try:
self.store.audit("u1", "p1", "act", "d")
finally:
auth_mod.audit_on_conn = original
self.assertEqual(calls["n"], 2)
self.assertEqual(len(self.store.list_audit("p1")), 1)
ledger_rows = self.store.conn.execute(
"SELECT COUNT(*) FROM audit_ledger").fetchone()[0]
self.assertEqual(ledger_rows, 1)
def _broken(conn, *args, **kwargs):
raise RuntimeError("ledger boom")
auth_mod.audit_on_conn = _broken
try:
with self.assertRaises(RuntimeError) as caught:
self.store.audit("u1", "p1", "act", "d2")
finally:
auth_mod.audit_on_conn = original
self.assertEqual(str(caught.exception), "ledger boom")
self.assertEqual(len(self.store.list_audit("p1")), 1)
ledger_rows = self.store.conn.execute(
"SELECT COUNT(*) FROM audit_ledger").fetchone()[0]
self.assertEqual(ledger_rows, 1)
def test_audit_append_only(self):
project_id, owner_id, _v, _c, _m, _o = self._project_with_roles()
self.assertFalse(hasattr(AuthStore, "delete_audit"))
self.assertFalse(hasattr(AuthStore, "update_audit"))
before = len(self.store.list_audit(project_id))
self.store.audit(owner_id, project_id, "create_task", "task-1")
self.store.audit(owner_id, project_id, "approve_merge", "task-1")
entries = self.store.list_audit(project_id)
self.assertEqual(len(entries), before + 2)
# 最新两条在前(倒序),且均为本次追加的动作
actions = [e["action"] for e in entries[:2]]
self.assertEqual(actions, ["approve_merge", "create_task"])
# 审计条目为 dict 且含关键字段
self.assertIn("user_id", entries[0])
self.assertIn("created_at", entries[0])
if __name__ == "__main__":
unittest.main()