Merge remote-tracking branches 'origin/main_laptop_1', 'origin/sub_laptop_1' and 'origin/sub_laptop_2' into sub_laptop_7
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -88,7 +89,7 @@ def test_html_은_먼저_막음(html) -> None:
|
||||
def root(monkeypatch):
|
||||
path = Path(tempfile.mkdtemp())
|
||||
(path / "B05").mkdir()
|
||||
(path / "B05" / "00_설명서.md").write_text("# 종단\n\n첫 문단\n", encoding="utf-8")
|
||||
(path / "B05" / "00_설명서.md").write_bytes("# 종단\n\n첫 문단\n".encode())
|
||||
monkeypatch.setattr(help_router, "HELP_ROOT", path)
|
||||
yield path
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
@@ -130,3 +131,143 @@ def test_그림_내려주기_no_cache(root) -> None:
|
||||
def test_그림_404(root, url) -> None:
|
||||
(root / "B05" / "full.webp").write_bytes(b"RIFF0000WEBP")
|
||||
assert client().get(url).status_code == 404
|
||||
|
||||
|
||||
# ── 538-3 글 바로 고침 ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def put(c: TestClient, version: str, start: int, end: int, text: str, key: str = "00_설명서"):
|
||||
body = {"version": version, "start": start, "end": end, "text": text}
|
||||
return c.put(f"/api/help/B05/{key}", json=body)
|
||||
|
||||
|
||||
def version_of(root: Path) -> str:
|
||||
return help_router.file_version((root / "B05" / "00_설명서.md").read_bytes())
|
||||
|
||||
|
||||
def test_그_줄만_갈고_새_판(root) -> None:
|
||||
old = version_of(root)
|
||||
res = put(client(), old, 2, 2, "고친 문단\n둘째 줄")
|
||||
assert res.status_code == 200
|
||||
item = res.json()["item"]
|
||||
text = (root / "B05" / "00_설명서.md").read_bytes().decode()
|
||||
assert text == "# 종단\n\n고친 문단\n둘째 줄\n" == item["text"]
|
||||
assert item["version"] == version_of(root) != old
|
||||
assert put(client(), item["version"], 2, 3, "").status_code == 200 # 빈 글 = 그 줄 지움
|
||||
assert (root / "B05" / "00_설명서.md").read_bytes().decode() == "# 종단\n\n"
|
||||
|
||||
|
||||
def test_crlf_bom_그대로(root) -> None:
|
||||
path = root / "B05" / "00_설명서.md"
|
||||
path.write_bytes(b"\xef\xbb\xbf# A\r\n\r\nB\r\n")
|
||||
assert put(client(), version_of(root), 2, 2, "C").status_code == 200
|
||||
assert path.read_bytes() == b"\xef\xbb\xbf# A\r\n\r\nC\r\n"
|
||||
|
||||
|
||||
def test_판_다르면_409_파일_그대로(root) -> None:
|
||||
before = (root / "B05" / "00_설명서.md").read_bytes()
|
||||
assert put(client(), "0" * 16, 2, 2, "남의 글").status_code == 409
|
||||
assert (root / "B05" / "00_설명서.md").read_bytes() == before
|
||||
|
||||
|
||||
def test_시스템_관리자만_403(root) -> None:
|
||||
before = (root / "B05" / "00_설명서.md").read_bytes()
|
||||
for role in ["USER", "COMPANY_ADMIN"]:
|
||||
assert put(client(role), version_of(root), 2, 2, "x").status_code == 403
|
||||
assert (root / "B05" / "00_설명서.md").read_bytes() == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("start", "end", "key", "code"),
|
||||
[
|
||||
(3, 2, "00_설명서", 400),
|
||||
(0, 9, "00_설명서", 400),
|
||||
(0, 0, "없음", 404),
|
||||
(0, 0, "_manual", 404),
|
||||
(0, 0, "..%2F..%2FCLAUDE", 404),
|
||||
],
|
||||
)
|
||||
def test_나쁜_범위_항목(root, start, end, key, code) -> None:
|
||||
assert put(client(), version_of(root), start, end, "x", key).status_code == code
|
||||
|
||||
|
||||
def test_원자_쓰기_실패해도_원본_그대로(root, monkeypatch) -> None:
|
||||
before = (root / "B05" / "00_설명서.md").read_bytes()
|
||||
|
||||
def boom(*_args):
|
||||
raise OSError("쓰기 실패")
|
||||
|
||||
monkeypatch.setattr("common_util.common_util_atomic.os.replace", boom)
|
||||
with pytest.raises(OSError):
|
||||
put(client(), version_of(root), 2, 2, "반쯤")
|
||||
assert (root / "B05" / "00_설명서.md").read_bytes() == before
|
||||
assert sorted(p.name for p in (root / "B05").iterdir()) == [
|
||||
"00_설명서.md"
|
||||
] # 임시 파일 남지 않음
|
||||
|
||||
|
||||
# ── 538-4 그림 바꾸기 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def png(width: int = 40, height: int = 20) -> bytes:
|
||||
from PIL import Image
|
||||
|
||||
out = io.BytesIO()
|
||||
Image.new("RGB", (width, height), (200, 30, 30)).save(out, "PNG")
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def upload(c: TestClient, name: str, blob: bytes, filename: str = "고른.png", page: str = "B05"):
|
||||
return c.post(
|
||||
f"/api/help/{page}/img",
|
||||
data={"name": name},
|
||||
files={"file": (filename, blob, "application/octet-stream")},
|
||||
)
|
||||
|
||||
|
||||
def test_그림_webp_로_덮고_manual_에_적음(root) -> None:
|
||||
from PIL import Image
|
||||
|
||||
(root / "B05" / "full.webp").write_bytes(b"old")
|
||||
res = upload(client(), "full.webp", png(3000, 1500))
|
||||
assert res.status_code == 200
|
||||
body = res.json()
|
||||
with Image.open(root / "B05" / "full.webp") as img:
|
||||
assert (img.format, img.size) == ("WEBP", (1800, 900)) # 폭 1800 넘으면 줄임
|
||||
assert body == {
|
||||
"name": "full.webp",
|
||||
"version": help_router.image_version(root / "B05" / "full.webp"),
|
||||
}
|
||||
assert upload(client(), "new_one.webp", png(), "a.jpg").status_code == 200 # 새 이름도 받음
|
||||
manual = json.loads((root / "B05" / "_manual.json").read_text(encoding="utf-8"))
|
||||
assert manual == ["full.webp", "new_one.webp"]
|
||||
assert [i["key"] for i in client().get("/api/help").json()["pages"][0]["items"]] == [
|
||||
"00_설명서"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "blob", "filename", "code"),
|
||||
[
|
||||
("full.webp", png(), "고른.gif", 400), # 확장자
|
||||
("full.webp", b"not an image at all", "고른.png", 400), # 그림 아님
|
||||
("full.webp", b"", "고른.png", 400),
|
||||
("../full.webp", png(), "고른.png", 404), # 경로 탈출
|
||||
("full.gif", png(), "고른.png", 404),
|
||||
("한글.webp", png(), "고른.png", 404),
|
||||
],
|
||||
)
|
||||
def test_그림_거절(root, name, blob, filename, code) -> None:
|
||||
assert upload(client(), name, blob, filename).status_code == code
|
||||
assert sorted(p.name for p in (root / "B05").iterdir()) == ["00_설명서.md"]
|
||||
|
||||
|
||||
def test_그림_5MB_넘으면_거절(root, monkeypatch) -> None:
|
||||
monkeypatch.setattr(help_router, "MAX_IMG_BYTES", 1000)
|
||||
assert upload(client(), "full.webp", png(400, 400) + b"\0" * 2000).status_code == 400
|
||||
|
||||
|
||||
def test_그림_시스템_관리자만_403(root) -> None:
|
||||
assert upload(client("USER"), "full.webp", png()).status_code == 403
|
||||
assert upload(client(), "full.webp", png(), page="..%2FB05").status_code == 404
|
||||
assert not (root / "B05" / "full.webp").exists()
|
||||
|
||||
@@ -28,7 +28,14 @@ def _folder(scenes, manual=None) -> Path:
|
||||
|
||||
def test_good_scene_passes():
|
||||
sc = _scene(
|
||||
before=[{"click": ".card"}, {"wait": ".modal"}, {"wait_ms": 300}, {"hover": ".tip"}],
|
||||
before=[
|
||||
{"click": ".card"},
|
||||
{"wait": ".modal"},
|
||||
{"wait_ms": 300},
|
||||
{"hover": ".tip"},
|
||||
{"select": "#kind", "value": "plan"},
|
||||
{"scroll": ".far"},
|
||||
],
|
||||
clip=".modal",
|
||||
callouts=[{"n": 1, "sel": ".a"}, {"n": 2, "sel": ".b", "pos": "br"}],
|
||||
after=[{"press": "Escape"}],
|
||||
@@ -57,6 +64,8 @@ def test_name_rule(name):
|
||||
{"callouts": [{"n": 1, "sel": ".a", "pos": "mid"}]},
|
||||
{"before": [{"click": ".a", "wait": ".b"}]},
|
||||
{"before": [{"fill": ".a"}]},
|
||||
{"before": [{"select": ".a"}]},
|
||||
{"before": [{"scroll": ""}]},
|
||||
{"before": [{"jump": ".a"}]},
|
||||
{"allow_post": ["projects"]},
|
||||
{"extra": 1},
|
||||
|
||||
Reference in New Issue
Block a user