Files
cowork-local/tests/test_keyring_adapter.py
Nam Pham Dinh ThanhandClaude Opus 5 d6dd6a030e feat(infra): AtomicJsonFile + KeyringAdapter, và đổi tên platform/ vì nó che stdlib
Ngày 21/08 của làn N1 (Nam): R02-T01 và R02-T04.

--- Lỗi phải sửa trước khi làm được gì ---

Kế hoạch đặt tên một tầng là platform/. Tôi dựng đúng theo đó sáng nay, có
kiểm "platform stdlib không bị che" và báo là an toàn. Kiểm đó SAI: tôi chỉ
thử từ thư mục cha. Chạy từ gốc repo — đúng cách 26 script trong tools/ và
scripts/ được gọi — thì platform/ che khuất platform của thư viện chuẩn, và
import keyring chết ngay:

    AttributeError: module 'platform' has no attribute 'system'

Nghĩa là R02-T04 không thể làm được chừng nào thư mục đó còn tên cũ. Đổi
platform/ -> adapters/. Đây là lệch khỏi plan.md và ảnh hưởng Team Hoa (họ sở
hữu platform/qt/qt_scheduler_clock.py) — đã ghi vào GammaTeam_decisions.md.

tests/test_no_stdlib_shadow.py chặn lỗi tái diễn, hai lớp: một bài so tên thư
mục gốc repo với sys.stdlib_module_names, một bài chạy tiến trình con với cwd
là gốc repo rồi import keyring thật. Dựng lại platform/ là cả hai đỏ.

--- R02-T01: AtomicJsonFile ---

config.py::save() đang gọi path.write_text(), tức là cắt file về 0 byte rồi
mới ghi. Chết giữa chừng là mất sạch cấu hình. Thay bằng: ghi file tạm cùng
thư mục -> flush + fsync -> os.replace (nguyên tử trên cả Windows và POSIX).

Test tiêm lỗi đúng như cột nghiệm thu của plan.md: cho os.replace ném lỗi
ngay bước cuối rồi khẳng định file cũ còn nguyên. Chỉ test "ghi rồi đọc lại"
thì write_text() cũ cũng qua — mà đó chính là thứ đang thay.

Phần đọc: file hỏng được dời thành .bad-<thời điểm> rồi trả mặc định. Giữ
đúng hành vi "hỏng cấu hình không chặn khởi động" của config.py, thêm phần
cứu được bản hỏng.

--- R02-T04: KeyringAdapter ---

Windows Credential Manager / macOS Keychain / Linux Secret Service. Không bao
giờ ném lỗi: máy không có kho (Linux headless, CI) thì available=False và trả
None, để tầng UI nói "chưa lưu được khoá" thay vì sập app. Test tiêm backend
giả, không đụng keyring thật của máy chạy test.

119 test xanh (102 + 17 mới). CASAN Check 1 sạch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 22:44:34 +09:00

94 lines
2.9 KiB
Python

"""KeyringAdapter — R02-T04.
Không đụng vào keyring thật của máy chạy test: tiêm một backend giả. Test mà
ghi vào Credential Manager thật thì để lại rác trên máy người khác, và trên CI
thì không có kho nào để ghi.
"""
from __future__ import annotations
import pytest
from cowork_local.infrastructure.secrets.keyring_adapter import KeyringAdapter
from cowork_local.infrastructure.secrets.secret_store import SecretStore, provider_key
class _KeyringGia:
"""Đủ giống thư viện keyring để adapter dùng được."""
def __init__(self, hong: bool = False):
self.kho: dict[tuple[str, str], str] = {}
self.hong = hong
def get_password(self, service, key):
if self.hong:
raise RuntimeError("kho bí mật không phản hồi")
return self.kho.get((service, key))
def set_password(self, service, key, value):
if self.hong:
raise RuntimeError("kho bí mật không phản hồi")
self.kho[(service, key)] = value
def delete_password(self, service, key):
if self.hong:
raise RuntimeError("kho bí mật không phản hồi")
del self.kho[(service, key)]
@pytest.fixture
def store():
a = KeyringAdapter(service="test-cowork")
a._backend = _KeyringGia()
a._available = True
return a
def test_khop_hop_dong_secret_store(store):
assert isinstance(store, SecretStore)
def test_luu_doc_xoa(store):
k = provider_key("openai")
assert store.get(k) is None
assert store.has(k) is False
store.set(k, "sk-that-la-bi-mat")
assert store.get(k) == "sk-that-la-bi-mat"
assert store.has(k) is True
store.delete(k)
assert store.get(k) is None
def test_moi_provider_mot_khoa_rieng(store):
store.set(provider_key("openai"), "khoa-openai")
store.set(provider_key("anthropic"), "khoa-anthropic")
assert store.get(provider_key("openai")) == "khoa-openai"
assert store.get(provider_key("anthropic")) == "khoa-anthropic"
def test_may_khong_co_kho_thi_im_lang_chu_khong_sap():
"""Linux headless và CI không có Secret Service. App vẫn phải chạy."""
a = KeyringAdapter(service="test-cowork")
a._backend = None
a._available = False
assert a.available is False
assert a.get("bat-ky") is None
a.set("bat-ky", "gia-tri") # không ném lỗi
a.delete("bat-ky") # không ném lỗi
assert a.has("bat-ky") is False
def test_kho_loi_giua_chung_thi_khong_lam_sap_app(store):
"""Keyring có thể hỏng lúc đang chạy — mất DBus, người dùng khoá máy."""
store._backend.hong = True
assert store.get("x") is None # nuốt lỗi, trả None
store.set("x", "y") # nuốt lỗi
store.delete("x") # nuốt lỗi
def test_xoa_khoa_khong_ton_tai_thi_bo_qua(store):
store.delete(provider_key("chua-bao-gio-luu")) # không ném lỗi