"""AtomicJsonFile — R02-T01. Test tiêm lỗi, đúng như cột nghiệm thu của plan.md. Cách kiểm: cắt ngang giữa lúc ghi rồi khẳng định file cũ **còn nguyên**. Nếu chỉ test "ghi rồi đọc lại thấy đúng" thì `path.write_text()` cũ cũng qua — mà đó chính là thứ ta đang thay. """ from __future__ import annotations import json import os import pytest from cowork_local.infrastructure.persistence.json.atomic_json_file import AtomicJsonFile def test_ghi_roi_doc_lai(tmp_path): f = AtomicJsonFile(tmp_path / "cau_hinh.json") f.write({"theme": "dark", "ngôn ngữ": "vi"}) assert f.read() == {"theme": "dark", "ngôn ngữ": "vi"} def test_chua_co_file_thi_tra_mac_dinh(tmp_path): f = AtomicJsonFile(tmp_path / "chua-ton-tai.json") assert f.read(default={"theme": "dark"}) == {"theme": "dark"} assert f.exists() is False def test_chet_giua_luc_ghi_thi_file_cu_con_nguyen(tmp_path, monkeypatch): """Lõi của R02-T01. Giả lập mất điện đúng lúc: cho ``os.replace`` ném lỗi. Đây là bước cuối cùng, tức là dữ liệu mới đã nằm trong file tạm rồi — nếu cài đặt sai theo kiểu ghi đè thẳng, file đích lúc này đã hỏng. """ path = tmp_path / "cau_hinh.json" f = AtomicJsonFile(path) f.write({"phiên bản": 1, "quan trọng": "đừng mất"}) def no_dien(*args, **kwargs): raise OSError("mô phỏng mất điện") monkeypatch.setattr(os, "replace", no_dien) with pytest.raises(OSError): f.write({"phiên bản": 2}) # bản cũ phải còn y nguyên assert f.read() == {"phiên bản": 1, "quan trọng": "đừng mất"} def test_khong_de_lai_rac_tmp_khi_ghi_hong(tmp_path, monkeypatch): path = tmp_path / "cau_hinh.json" f = AtomicJsonFile(path) f.write({"a": 1}) monkeypatch.setattr(os, "replace", lambda *a, **k: (_ for _ in ()).throw(OSError("x"))) with pytest.raises(OSError): f.write({"a": 2}) con_lai = [p.name for p in tmp_path.iterdir()] assert con_lai == ["cau_hinh.json"], f"còn rác: {con_lai}" def test_file_hong_thi_cach_ly_va_tra_mac_dinh(tmp_path): """Hỏng cấu hình không được chặn khởi động — giữ đúng hành vi config.py hiện tại, nhưng thêm phần giữ lại bản hỏng để còn cứu.""" path = tmp_path / "cau_hinh.json" path.write_text("{ đây không phải json", encoding="utf-8") f = AtomicJsonFile(path) assert f.read(default={"theme": "dark"}) == {"theme": "dark"} assert not path.exists(), "file hỏng phải được dời đi" bad = list(tmp_path.glob("*.bad-*")) assert len(bad) == 1, "phải giữ lại bản hỏng để cứu tay" assert "đây không phải json" in bad[0].read_text(encoding="utf-8") def test_ghi_de_nhieu_lan_van_dung(tmp_path): f = AtomicJsonFile(tmp_path / "dem.json") for i in range(20): f.write({"lần": i}) assert f.read() == {"lần": 19} assert list(tmp_path.glob("*.tmp")) == [] def test_giu_nguyen_tieng_viet_khong_escape(tmp_path): """config.py hiện dùng ensure_ascii=False — giữ nguyên để file đọc được bằng mắt và git diff không thành một đống \\uXXXX.""" path = tmp_path / "vi.json" AtomicJsonFile(path).write({"tên": "Nguyễn Văn Đức"}) raw = path.read_text(encoding="utf-8") assert "Nguyễn Văn Đức" in raw assert "\\u" not in raw def test_tao_thu_muc_cha_neu_chua_co(tmp_path): f = AtomicJsonFile(tmp_path / "sâu" / "hơn" / "nữa" / "c.json") f.write({"ok": True}) assert f.read() == {"ok": True} def test_json_ghi_ra_doc_duoc_bang_thu_vien_chuan(tmp_path): path = tmp_path / "c.json" AtomicJsonFile(path).write({"n": [1, 2, {"m": None}]}) assert json.loads(path.read_text(encoding="utf-8")) == {"n": [1, 2, {"m": None}]}